From dffcee55e3d18ee367c334b252ccafd10703bb0c Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Mon, 27 Oct 2025 10:25:13 -0400 Subject: [PATCH 01/10] feat!: update aac 2017 models close #42 --- .gitmodules | 2 +- src/ga4gh/va_spec/aac_2017/__init__.py | 16 +- src/ga4gh/va_spec/aac_2017/models.py | 168 +++++++++++----- src/ga4gh/va_spec/base/__init__.py | 2 + src/ga4gh/va_spec/base/core.py | 244 ++++++++++-------------- src/ga4gh/va_spec/ccv_2022/__init__.py | 4 +- src/ga4gh/va_spec/ccv_2022/models.py | 2 +- submodules/va_spec | 2 +- tests/validation/test_va_spec_models.py | 135 +++++++++++-- 9 files changed, 354 insertions(+), 221 deletions(-) diff --git a/.gitmodules b/.gitmodules index 433d259..64cd7b9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "submodules/va_spec"] path = submodules/va_spec url = https://github.com/ga4gh/va-spec - branch = 1.0 + branch = issue-293 diff --git a/src/ga4gh/va_spec/aac_2017/__init__.py b/src/ga4gh/va_spec/aac_2017/__init__.py index d26bc8e..60393d5 100644 --- a/src/ga4gh/va_spec/aac_2017/__init__.py +++ b/src/ga4gh/va_spec/aac_2017/__init__.py @@ -1,21 +1,21 @@ """Module to load and init namespace at package level.""" from .models import ( - AMP_ASCO_CAP_LEVELS, + AMP_ASCO_CAP_STRENGTHS, AMP_ASCO_CAP_TIERS, Classification, Strength, - VariantDiagnosticStudyStatement, - VariantPrognosticStudyStatement, - VariantTherapeuticResponseStudyStatement, + VariantDiagnosticStatement, + VariantPrognosticStatement, + VariantTherapeuticResponseStatement, ) __all__ = [ - "AMP_ASCO_CAP_LEVELS", + "AMP_ASCO_CAP_STRENGTHS", "AMP_ASCO_CAP_TIERS", "Classification", "Strength", - "VariantDiagnosticStudyStatement", - "VariantPrognosticStudyStatement", - "VariantTherapeuticResponseStudyStatement", + "VariantDiagnosticStatement", + "VariantPrognosticStatement", + "VariantTherapeuticResponseStatement", ] diff --git a/src/ga4gh/va_spec/aac_2017/models.py b/src/ga4gh/va_spec/aac_2017/models.py index 98028c4..fac1637 100644 --- a/src/ga4gh/va_spec/aac_2017/models.py +++ b/src/ga4gh/va_spec/aac_2017/models.py @@ -4,17 +4,20 @@ sequence variants in cancer. """ +from abc import ABC from enum import Enum +from typing import Self -from pydantic import ( - Field, - field_validator, -) +from pydantic import Field, model_validator +from pydantic.dataclasses import dataclass from ga4gh.core.models import MappableConcept, iriReference from ga4gh.va_spec.base.core import ( + Direction, + EvidenceLine, Method, Statement, + VariantClinicalSignificanceProposition, VariantDiagnosticProposition, VariantPrognosticProposition, VariantTherapeuticResponseProposition, @@ -26,71 +29,140 @@ class Strength(str, Enum): """Define constraints for AMP/ASCO/CAP strength coding""" - LEVEL_A = "Level A" - LEVEL_B = "Level B" - LEVEL_C = "Level C" - LEVEL_D = "Level D" + STRONG = "strong" + POTENTIAL = "potential" -AMP_ASCO_CAP_LEVELS = [v.value for v in Strength.__members__.values()] +AMP_ASCO_CAP_STRENGTHS = [v.value for v in Strength.__members__.values()] class Classification(str, Enum): """Define constraints for AMP/ASCO/CAP classification coding""" - TIER_I = "Tier I" - TIER_II = "Tier II" - TIER_III = "Tier III" - TIER_IV = "Tier IV" + TIER_1 = "tier 1" + TIER_2 = "tier 2" + TIER_3 = "tier 3" + TIER_4 = "tier 4" AMP_ASCO_CAP_TIERS = [v.value for v in Classification.__members__.values()] -class AmpAscoCapValidatorMixin: - """Mixin class for reusable AMP/ASCO/CAP field validators - - Should be used with classes that inherit from Statement - """ - - @field_validator("strength") - @classmethod - def validate_strength(cls, v: MappableConcept | None) -> MappableConcept | None: - """Validate strength - - :param v: Strength - :raises ValueError: If invalid strength values are provided - :return: Validated strength value - """ - return validate_mappable_concept( - v, +class ClassificationName(str, Enum): + """Define constraints for AMP/ASCO/CAP classification name""" + + TIER_1 = "Tier I" + TIER_2 = "Tier II" + TIER_3 = "Tier III" + TIER_4 = "Tier IV" + + +@dataclass +class AmpAscoCapConfig: + """AMP/ASCO/CAP config for expected values""" + + name: ClassificationName + direction: Direction + strength: Strength | None + + +CLASSIFICATION_POLICY_MAP = { + Classification.TIER_1: AmpAscoCapConfig( + name=ClassificationName.TIER_1, + direction=Direction.SUPPORTS, + strength=Strength.STRONG, + ), + Classification.TIER_2: AmpAscoCapConfig( + name=ClassificationName.TIER_2, + direction=Direction.SUPPORTS, + strength=Strength.POTENTIAL, + ), + Classification.TIER_3: AmpAscoCapConfig( + name=ClassificationName.TIER_3, direction=Direction.NEUTRAL, strength=None + ), + Classification.TIER_4: AmpAscoCapConfig( + name=ClassificationName.TIER_4, direction=Direction.DISPUTES, strength=None + ), +} + + +class _AmpAscoCapStatement(Statement, ABC): + """Abstract base class for AAC 2017 statements""" + + @model_validator(mode="after") + def validate_aac_statement(self) -> Self: + """Validate AMP/ASCO/CAP statements""" + + def _validate_classification_config( + classification_code: Classification, + classification_name: str, + direction: str, + strength_code: str | None, + ) -> None: + """Validate that classificati""" + expected_config = CLASSIFICATION_POLICY_MAP[classification_code] + if strength_code != expected_config.strength: + expected_strength = ( + expected_config.strength.value + if expected_config.strength + else expected_config.strength + ) + msg = f"`strength` must be: {expected_strength}" + raise ValueError(msg) + + if classification_name != expected_config.name: + msg = f"`classification.name` must be: {expected_config.name.value}" + raise ValueError(msg) + + if direction != expected_config.direction: + msg = f"`direction` must be: {expected_config.direction.value}" + raise ValueError(msg) + + # Validate strength + validate_mappable_concept( + self.strength, System.AMP_ASCO_CAP, - valid_codes=AMP_ASCO_CAP_LEVELS, mc_is_required=False, ) - @field_validator("classification") - @classmethod - def validate_classification(cls, v: MappableConcept) -> MappableConcept: - """Validate classification - - :param v: Classification - :raises ValueError: If invalid classification values are provided - :return: Validated classification value - """ - return validate_mappable_concept( - v, System.AMP_ASCO_CAP, valid_codes=AMP_ASCO_CAP_TIERS + # Validate classification + validate_mappable_concept( + self.classification, + System.AMP_ASCO_CAP, + valid_codes=AMP_ASCO_CAP_TIERS, + mc_is_required=True, + ) + _validate_classification_config( + Classification(self.classification.primaryCoding.code.root), + self.classification.name, + self.direction, + self.strength.primaryCoding.code.root, ) + # Validate hasEvidenceLines + for evidence_line in self.hasEvidenceLines or []: + if isinstance(evidence_line, EvidenceLine): + target_proposition = evidence_line.targetProposition + if target_proposition and not isinstance( + target_proposition, + VariantDiagnosticProposition + | VariantPrognosticProposition + | VariantTherapeuticResponseProposition, + ): + msg = "`targetProposition` must be one of: `VariantDiagnosticProposition`, `VariantPrognosticProposition`, `VariantTherapeuticResponseProposition`" + raise ValueError(msg) + + return self + -class VariantDiagnosticStudyStatement(Statement, AmpAscoCapValidatorMixin): +class VariantDiagnosticStatement(_AmpAscoCapStatement): """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 results. """ - proposition: VariantDiagnosticProposition = Field( + proposition: VariantClinicalSignificanceProposition = Field( ..., description="A proposition about a diagnostic association between a variant and condition, for which the study provides evidence. The validity of this proposition, and the level of confidence/evidence supporting it, may be assessed and reported by the Statement.", ) @@ -108,13 +180,13 @@ class VariantDiagnosticStudyStatement(Statement, AmpAscoCapValidatorMixin): ) -class VariantPrognosticStudyStatement(Statement, AmpAscoCapValidatorMixin): +class VariantPrognosticStatement(_AmpAscoCapStatement): """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. """ - proposition: VariantPrognosticProposition = Field( + proposition: VariantClinicalSignificanceProposition = Field( ..., description="A proposition about a prognostic association between a variant and condition, for which the study provides evidence. The validity of this proposition, and the level of confidence/evidence supporting it, may be assessed and reported by the Statement.", ) @@ -132,13 +204,13 @@ class VariantPrognosticStudyStatement(Statement, AmpAscoCapValidatorMixin): ) -class VariantTherapeuticResponseStudyStatement(Statement, AmpAscoCapValidatorMixin): +class VariantTherapeuticResponseStatement(_AmpAscoCapStatement): """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. """ - proposition: VariantTherapeuticResponseProposition = Field( + proposition: VariantClinicalSignificanceProposition = Field( ..., description="A proposition about the therapeutic response associated with a variant, for which the study provides evidence. The validity of this proposition, and the level of confidence/evidence supporting it, may be assessed and reported by the Statement.", ) diff --git a/src/ga4gh/va_spec/base/__init__.py b/src/ga4gh/va_spec/base/__init__.py index 79d710f..85489ce 100644 --- a/src/ga4gh/va_spec/base/__init__.py +++ b/src/ga4gh/va_spec/base/__init__.py @@ -20,6 +20,7 @@ StudyResult, SubjectVariantProposition, TumorVariantFrequencyStudyResult, + VariantClinicalSignificanceProposition, VariantDiagnosticProposition, VariantOncogenicityProposition, VariantPathogenicityProposition, @@ -81,6 +82,7 @@ "Therapeutic", "TherapeuticResponsePredicate", "TherapyGroup", + "VariantClinicalSignificanceProposition", "VariantDiagnosticProposition", "VariantOncogenicityProposition", "VariantPathogenicityProposition", diff --git a/src/ga4gh/va_spec/base/core.py b/src/ga4gh/va_spec/base/core.py index e90fa7d..3062bb7 100644 --- a/src/ga4gh/va_spec/base/core.py +++ b/src/ga4gh/va_spec/base/core.py @@ -2,19 +2,16 @@ from __future__ import annotations -import importlib -import inspect from abc import ABC from datetime import date, datetime from enum import Enum -from typing import Annotated, Literal, TypeVar +from typing import Annotated, Literal from pydantic import ( ConfigDict, Field, RootModel, StringConstraints, - ValidationError, field_validator, ) @@ -35,9 +32,6 @@ from ga4gh.va_spec.base.validators import validate_mappable_concept from ga4gh.vrs.models import Allele, MolecularVariation -StatementType = TypeVar("StatementType") -EvidenceLineType = TypeVar("EvidenceLineType") - class CoreType(str, Enum): """Define VA Spec Base Core Types""" @@ -52,6 +46,22 @@ class CoreType(str, Enum): STUDY_GROUP = "StudyGroup" +class Agent(Entity, BaseModelForbidExtra): + """An autonomous actor (person, organization, or software agent) that bears some + form of responsibility for an activity taking place, for the existence of an entity, + or for another agent's activity. + """ + + type: Literal["Agent"] = Field( + default=CoreType.AGENT.value, description=f"MUST be '{CoreType.AGENT.value}'." + ) + name: str | None = Field(default=None, description="The given name of the Agent.") + agentType: str | None = Field( + default=None, + description="A specific type of agent the Agent object represents. Recommended subtypes include codes for `person`, `organization`, or `software`.", + ) + + class Contribution(Entity, BaseModelForbidExtra): """An action taken by an agent in contributing to the creation, modification, assessment, or deprecation of a particular entity (e.g. a Statement, EvidenceLine, @@ -145,6 +155,57 @@ class InformationEntity(Entity): ) +class StudyGroup(Entity, BaseModelForbidExtra): + """A collection of individuals or specimens from the same taxonomic class, selected + for analysis in a scientific study based on their exhibiting one or more common + characteristics (e.g. species, race, age, gender, disease state, income). May be + referred to as a 'cohort' or 'population' in specific research settings. + """ + + type: Literal["StudyGroup"] = Field( + default=CoreType.STUDY_GROUP.value, + description=f"Must be '{CoreType.STUDY_GROUP.value}'", + ) + memberCount: int | None = Field( + default=None, + description="The total number of individual members in the StudyGroup.", + ) + characteristics: list[MappableConcept] | None = Field( + default=None, + description="A feature or role shared by all members of the StudyGroup, representing a criterion for membership in the group.", + ) + + +class DataSet(Entity, BaseModelForbidExtra): + """A collection of related data items or records that are organized together in a + common format or structure, to enable their computational manipulation as a unit. + """ + + type: Literal["DataSet"] = Field( + default=CoreType.DATA_SET.value, + description=f"MUST be '{CoreType.DATA_SET.value}'.", + ) + datasetType: str | None = Field( + default=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')", + ) + reportedIn: Document | iriReference | None = Field( + default=None, description="A document in which the the Method is reported." + ) + releaseDate: date | None = Field( + default=None, + description="Indicates the date a version of a DataSet was formally released.", + ) + version: str | None = Field( + default=None, + description="The version of the DataSet, as assigned by its creator.", + ) + license: MappableConcept | None = Field( + default=None, + description="A specific license that dictates legal permissions for how a data set can be used (by whom, where, for what purposes, with what additional requirements, etc.)", + ) + + class _StudyResult(InformationEntity, ABC): """A collection of data items from a single study that pertain to a particular subject or experimental unit in the study, along with optional provenance information @@ -312,6 +373,7 @@ class SubjectVariantProposition(RootModel): | VariantPrognosticProposition | VariantOncogenicityProposition | VariantTherapeuticResponseProposition + | VariantTherapeuticResponseProposition ) = Field(discriminator="type") @@ -359,6 +421,27 @@ class ExperimentalVariantFunctionalImpactProposition( ) +class VariantClinicalSignificanceProposition( + ClinicalVariantProposition, BaseModelForbidExtra +): + """A Proposition describing the clinical significance of a variant with respect to a + condition. + """ + + model_config = ConfigDict(use_enum_values=True) + type: Literal["VariantClinicalSignificanceProposition"] = Field( + default="VariantClinicalSignificanceProposition", + description="MUST be 'VariantClinicalSignificanceProposition'.", + ) + predicate: Literal["hasClinicalSignificanceFor"] = Field( + default="hasClinicalSignificanceFor", + description="The predicate associating the subject variant to clinical significance for the object Condition. MUST be 'hasClinicalSignificanceFor'.", + ) + objectCondition: Condition | iriReference = Field( + ..., description="The disease that is evaluated." + ) + + class VariantDiagnosticProposition(ClinicalVariantProposition, BaseModelForbidExtra): """A Proposition about whether a variant is associated with a disease (a diagnostic inclusion criterion), or absence of a disease (diagnostic exclusion criterion). @@ -464,22 +547,6 @@ class VariantTherapeuticResponseProposition( ) -class Agent(Entity, BaseModelForbidExtra): - """An autonomous actor (person, organization, or software agent) that bears some - form of responsibility for an activity taking place, for the existence of an entity, - or for another agent's activity. - """ - - type: Literal["Agent"] = Field( - default=CoreType.AGENT.value, description=f"MUST be '{CoreType.AGENT.value}'." - ) - name: str | None = Field(default=None, description="The given name of the Agent.") - agentType: str | None = Field( - default=None, - description="A specific type of agent the Agent object represents. Recommended subtypes include codes for `person`, `organization`, or `software`.", - ) - - class Direction(str, Enum): """A term indicating whether the Statement supports, disputes, or remains neutral w.r.t. the validity of the Proposition it evaluates. @@ -490,36 +557,6 @@ class Direction(str, Enum): DISPUTES = "disputes" -class DataSet(Entity, BaseModelForbidExtra): - """A collection of related data items or records that are organized together in a - common format or structure, to enable their computational manipulation as a unit. - """ - - type: Literal["DataSet"] = Field( - default=CoreType.DATA_SET.value, - description=f"MUST be '{CoreType.DATA_SET.value}'.", - ) - datasetType: str | None = Field( - default=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')", - ) - reportedIn: Document | iriReference | None = Field( - default=None, description="A document in which the the Method is reported." - ) - releaseDate: date | None = Field( - default=None, - description="Indicates the date a version of a DataSet was formally released.", - ) - version: str | None = Field( - default=None, - description="The version of the DataSet, as assigned by its creator.", - ) - license: MappableConcept | None = Field( - default=None, - description="A specific license that dictates legal permissions for how a data set can be used (by whom, where, for what purposes, with what additional requirements, etc.)", - ) - - class EvidenceLine(InformationEntity, BaseModelForbidExtra): """An independent, evidence-based argument that may support or refute the validity of a specific Proposition. The strength and direction of this argument is based on @@ -533,12 +570,21 @@ class EvidenceLine(InformationEntity, BaseModelForbidExtra): default=CoreType.EVIDENCE_LINE.value, description=f"MUST be '{CoreType.EVIDENCE_LINE.value}'.", ) - targetProposition: Proposition | SubjectVariantProposition | None = Field( + targetProposition: ( + ExperimentalVariantFunctionalImpactProposition + | VariantClinicalSignificanceProposition + | VariantDiagnosticProposition + | VariantOncogenicityProposition + | VariantPathogenicityProposition + | VariantPrognosticProposition + | VariantTherapeuticResponseProposition + | None + ) = Field( default=None, 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 | EvidenceLineType | iriReference] | None + list[StudyResult | Statement | EvidenceLine | iriReference] | None ) = Field( default=None, description="An individual piece of information that was evaluated as evidence in building the argument represented by an Evidence Line.", @@ -560,73 +606,6 @@ class EvidenceLine(InformationEntity, BaseModelForbidExtra): description="A term summarizing the overall outcome of the evidence assessment represented by the Evidence Line, in terms of the direction and strength of support it provides for or against the target Proposition.", ) - @field_validator("hasEvidenceItems", mode="before") - def validate_has_evidence_items( - cls, # noqa: N805 - v: list | None, - ) -> list | None: - """Ensure hasEvidenceItems is correct type - - This is needed since Pydantic was unable to determine which model to use - - This only handles cases defined in the VA-Spec. - - :param v: hasEvidenceItems value - :raises ValueError: If unable to find valid model for evidence items - :return: Evidence items - """ - if not v: - return v - - evidence_items = [] - - # Avoid circular imports - has_evidence_items_models = [] - for module in [ - "ga4gh.va_spec.aac_2017.models", - "ga4gh.va_spec.acmg_2015.models", - "ga4gh.va_spec.ccv_2022.models", - ]: - imported_module = importlib.import_module(module) - has_evidence_items_models.extend( - [ - obj_ - for _, obj_ in vars(imported_module).items() - if inspect.isclass(obj_) - and issubclass(obj_, Statement) - and obj_.__name__.endswith(("Statement", "EvidenceLine")) - and obj_ not in (Statement, EvidenceLine) - ] - ) - - has_evidence_items_models.extend( - [Statement, StudyResult, EvidenceLine, iriReference] - ) - - for evidence_item in v: - if isinstance(evidence_item, dict): - found_model = False - for evidence_item_model in has_evidence_items_models: - try: - evidence_item = evidence_item_model(**evidence_item) - except ValidationError: - pass - else: - evidence_items.append(evidence_item) - found_model = True - break - if not found_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)) - elif isinstance(evidence_item, tuple(has_evidence_items_models)): - evidence_items.append(evidence_item) - else: - err_msg = "Unable to find valid model for `hasEvidenceItems`" - raise ValueError(err_msg) - return evidence_items - @staticmethod def _validate_evidence_outcome( values: dict, system: System, code_pattern: str @@ -742,22 +721,5 @@ class Statement(InformationEntity, BaseModelForbidExtra): ) -class StudyGroup(Entity, BaseModelForbidExtra): - """A collection of individuals or specimens from the same taxonomic class, selected - for analysis in a scientific study based on their exhibiting one or more common - characteristics (e.g. species, race, age, gender, disease state, income). May be - referred to as a 'cohort' or 'population' in specific research settings. - """ - - type: Literal["StudyGroup"] = Field( - default=CoreType.STUDY_GROUP.value, - description=f"Must be '{CoreType.STUDY_GROUP.value}'", - ) - memberCount: int | None = Field( - default=None, - description="The total number of individual members in the StudyGroup.", - ) - characteristics: list[MappableConcept] | None = Field( - default=None, - description="A feature or role shared by all members of the StudyGroup, representing a criterion for membership in the group.", - ) +EvidenceLine.model_rebuild() +Statement.model_rebuild() diff --git a/src/ga4gh/va_spec/ccv_2022/__init__.py b/src/ga4gh/va_spec/ccv_2022/__init__.py index 8c6c7a9..a84a82c 100644 --- a/src/ga4gh/va_spec/ccv_2022/__init__.py +++ b/src/ga4gh/va_spec/ccv_2022/__init__.py @@ -2,10 +2,10 @@ from .models import ( VariantOncogenicityEvidenceLine, - VariantOncogenicityStudyStatement, + VariantOncogenicityStatement, ) __all__ = [ "VariantOncogenicityEvidenceLine", - "VariantOncogenicityStudyStatement", + "VariantOncogenicityStatement", ] diff --git a/src/ga4gh/va_spec/ccv_2022/models.py b/src/ga4gh/va_spec/ccv_2022/models.py index 3ba4c88..59f0594 100644 --- a/src/ga4gh/va_spec/ccv_2022/models.py +++ b/src/ga4gh/va_spec/ccv_2022/models.py @@ -98,7 +98,7 @@ def validate_model(cls, values: dict) -> dict: # noqa: N805 return cls._validate_evidence_outcome(values, System.CCV, ccv_code_pattern) -class VariantOncogenicityStudyStatement(Statement): +class VariantOncogenicityStatement(Statement): """A statement reporting a conclusion from a single study about whether a variant is associated with oncogenicity (positive or negative) - based on interpretation of the study's results. diff --git a/submodules/va_spec b/submodules/va_spec index da35aa0..f2379c7 160000 --- a/submodules/va_spec +++ b/submodules/va_spec @@ -1 +1 @@ -Subproject commit da35aa0286aa209b24e8d3a827ddd5a491ab5350 +Subproject commit f2379c75dc1af9eb06cdd60aa001266f3e2872d7 diff --git a/tests/validation/test_va_spec_models.py b/tests/validation/test_va_spec_models.py index 8a302c4..1a111f3 100644 --- a/tests/validation/test_va_spec_models.py +++ b/tests/validation/test_va_spec_models.py @@ -10,7 +10,7 @@ from ga4gh.core.models import Coding, MappableConcept, code, iriReference from ga4gh.va_spec import acmg_2015, base, ccv_2022 -from ga4gh.va_spec.aac_2017.models import VariantTherapeuticResponseStudyStatement +from ga4gh.va_spec.aac_2017.models import VariantTherapeuticResponseStatement from ga4gh.va_spec.acmg_2015.models import ( VariantPathogenicityEvidenceLine, VariantPathogenicityStatement, @@ -20,11 +20,17 @@ CohortAlleleFrequencyStudyResult, ExperimentalVariantFunctionalImpactStudyResult, ) -from ga4gh.va_spec.base.core import EvidenceLine, Method, StudyGroup, StudyResult +from ga4gh.va_spec.base.core import ( + EvidenceLine, + Method, + Statement, + StudyGroup, + StudyResult, +) from ga4gh.va_spec.base.domain_entities import ConditionSet from ga4gh.va_spec.ccv_2022.models import ( VariantOncogenicityEvidenceLine, - VariantOncogenicityStudyStatement, + VariantOncogenicityStatement, ) VA_SPEC_TESTS_DIR = SUBMODULES_DIR / "tests" @@ -234,13 +240,13 @@ def test_evidence_line(caf): "strength": { "primaryCoding": { "system": "AMP/ASCO/CAP (AAC) Guidelines, 2017", - "code": "Level A", + "code": "strong", } }, "classification": { "primaryCoding": { "system": "AMP/ASCO/CAP (AAC) Guidelines, 2017", - "code": "Tier I", + "code": "tier 1", } }, "specifiedBy": { @@ -262,7 +268,7 @@ def test_evidence_line(caf): } el = EvidenceLine(**el_dict) assert isinstance(el.hasEvidenceItems[0], iriReference) - assert isinstance(el.hasEvidenceItems[1], VariantTherapeuticResponseStudyStatement) + assert isinstance(el.hasEvidenceItems[1], Statement) el_dict = { "type": "EvidenceLine", @@ -303,9 +309,7 @@ def test_evidence_line(caf): "hasEvidenceItems": [Agent(name="Joe")], "directionOfEvidenceProvided": "supports", } - with pytest.raises( - ValueError, match="Unable to find valid model for `hasEvidenceItems`" - ): + with pytest.raises(ValueError, match="validation errors for EvidenceLine"): EvidenceLine(**invalid_params) invalid_params = { @@ -313,9 +317,7 @@ def test_evidence_line(caf): "hasEvidenceItems": [{"type": "Statement"}], "directionOfEvidenceProvided": "supports", } - with pytest.raises( - ValueError, match="Unable to find valid model for `hasEvidenceItems`" - ): + with pytest.raises(ValueError, match="validation errors for EvidenceLine"): EvidenceLine(**invalid_params) @@ -489,7 +491,7 @@ def test_variant_pathogenicity_el(): def test_variant_onco_stmt(): - """Ensure VariantOncogenicityStudyStatement model works as expected""" + """Ensure VariantOncogenicityStatement model works as expected""" params = { "direction": "neutral", "proposition": { @@ -512,33 +514,33 @@ def test_variant_onco_stmt(): } }, } - assert VariantOncogenicityStudyStatement(**params) + assert VariantOncogenicityStatement(**params) valid_params = deepcopy(params) valid_params["strength"] = None - assert VariantOncogenicityStudyStatement(**valid_params) + assert VariantOncogenicityStatement(**valid_params) invalid_params = deepcopy(params) invalid_params["strength"]["primaryCoding"]["code"] = "oncogenic" with pytest.raises(ValueError, match="`primaryCoding.code` must be one of"): - VariantOncogenicityStudyStatement(**invalid_params) + VariantOncogenicityStatement(**invalid_params) invalid_params = deepcopy(params) invalid_params["strength"]["primaryCoding"]["system"] = "ACMG Guidelines, 2015" with pytest.raises(ValueError, match="`primaryCoding.system` must be"): - VariantOncogenicityStudyStatement(**invalid_params) + VariantOncogenicityStatement(**invalid_params) invalid_params = deepcopy(params) invalid_params["classification"]["primaryCoding"]["code"] = "pathogenic" with pytest.raises(ValueError, match="`primaryCoding.code` must be one of"): - VariantOncogenicityStudyStatement(**invalid_params) + VariantOncogenicityStatement(**invalid_params) invalid_params = deepcopy(params) invalid_params["classification"]["primaryCoding"]["system"] = ( "ACMG Guidelines, 2015" ) with pytest.raises(ValueError, match="`primaryCoding.system` must be"): - VariantOncogenicityStudyStatement(**invalid_params) + VariantOncogenicityStatement(**invalid_params) def test_variant_onco_el(): @@ -609,6 +611,101 @@ def test_variant_onco_el(): VariantOncogenicityEvidenceLine(**invalid_params) +def test_aac_statement(): + """Test that AMP/ASCO/CAP statement model validators work correctly""" + params = { + "direction": "supports", + "proposition": { + "type": "VariantClinicalSignificanceProposition", + "predicate": "hasClinicalSignificanceFor", + "objectCondition": "conditions.json#/1", + "subjectVariant": "alleles.json#/1", + }, + "strength": { + "primaryCoding": { + "code": "strong", + "system": "AMP/ASCO/CAP (AAC) Guidelines, 2017", + } + }, + "specifiedBy": "documents.json#/1", + "classification": { + "name": "Tier I", + "primaryCoding": { + "code": "tier 1", + "system": "AMP/ASCO/CAP (AAC) Guidelines, 2017", + }, + }, + "hasEvidenceLines": [ + { + "directionOfEvidenceProvided": "supports", + "strengthOfEvidenceProvided": { + "primaryCoding": { + "code": "Level A", + "system": "AMP/ASCO/CAP (AAC) Guidelines, 2017", + } + }, + "hasEvidenceItems": [ + "evidence_lines.json#/1", + { + "type": "Statement", + "direction": "supports", + "proposition": { + "type": "VariantDiagnosticProposition", + "predicate": "isDiagnosticExclusionCriterionFor", + "objectCondition": "conditions.json#/1", + "subjectVariant": "alleles.json#/1", + }, + "strength": { + "primaryCoding": { + "code": "A", + "system": "System", + } + }, + "specifiedBy": "documents.json#/1", + }, + ], + }, + ], + } + assert VariantTherapeuticResponseStatement(**params) + + # Invalid strength + invalid_params = deepcopy(params) + invalid_params["strength"]["primaryCoding"]["code"] = "Strong" + with pytest.raises(ValidationError, match="`strength` must be: strong"): + VariantTherapeuticResponseStatement(**invalid_params) + + invalid_params = deepcopy(params) + invalid_params["strength"]["primaryCoding"]["code"] = "potential" + with pytest.raises(ValidationError, match="`strength` must be: strong"): + VariantTherapeuticResponseStatement(**invalid_params) + + # Invalid classification + invalid_params = deepcopy(params) + invalid_params["classification"]["primaryCoding"]["code"] = "Tier I" + with pytest.raises(ValidationError, match="`primaryCoding.code` must be one of"): + VariantTherapeuticResponseStatement(**invalid_params) + + invalid_params = deepcopy(params) + invalid_params["classification"]["name"] = "tier 1" + with pytest.raises(ValidationError, match="`classification.name` must be: Tier I"): + VariantTherapeuticResponseStatement(**invalid_params) + + # Invalid direction + invalid_params = deepcopy(params) + invalid_params["direction"] = "disputes" + with pytest.raises(ValidationError, match="`direction` must be: supports"): + VariantTherapeuticResponseStatement(**invalid_params) + + # Invalid targetProposition + invalid_params = deepcopy(params) + invalid_params["hasEvidenceLines"][0]["targetProposition"] = invalid_params[ + "proposition" + ] + with pytest.raises(ValidationError, match="`targetProposition` must be one of"): + VariantTherapeuticResponseStatement(**invalid_params) + + def test_examples(test_definitions): """Test VA Spec examples""" va_spec_schema_mapping = { From a8c92dd5bba1bd0d839d7308e72eb5a143d77eb1 Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Mon, 27 Oct 2025 10:28:24 -0400 Subject: [PATCH 02/10] fixes --- src/ga4gh/va_spec/aac_2017/models.py | 50 ++++++++++++++++------------ 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/src/ga4gh/va_spec/aac_2017/models.py b/src/ga4gh/va_spec/aac_2017/models.py index fac1637..78bf63a 100644 --- a/src/ga4gh/va_spec/aac_2017/models.py +++ b/src/ga4gh/va_spec/aac_2017/models.py @@ -6,6 +6,7 @@ from abc import ABC from enum import Enum +from types import MappingProxyType from typing import Self from pydantic import Field, model_validator @@ -66,24 +67,26 @@ class AmpAscoCapConfig: strength: Strength | None -CLASSIFICATION_POLICY_MAP = { - Classification.TIER_1: AmpAscoCapConfig( - name=ClassificationName.TIER_1, - direction=Direction.SUPPORTS, - strength=Strength.STRONG, - ), - Classification.TIER_2: AmpAscoCapConfig( - name=ClassificationName.TIER_2, - direction=Direction.SUPPORTS, - strength=Strength.POTENTIAL, - ), - Classification.TIER_3: AmpAscoCapConfig( - name=ClassificationName.TIER_3, direction=Direction.NEUTRAL, strength=None - ), - Classification.TIER_4: AmpAscoCapConfig( - name=ClassificationName.TIER_4, direction=Direction.DISPUTES, strength=None - ), -} +CLASSIFICATION_POLICY_MAP = MappingProxyType( + { + Classification.TIER_1: AmpAscoCapConfig( + name=ClassificationName.TIER_1, + direction=Direction.SUPPORTS, + strength=Strength.STRONG, + ), + Classification.TIER_2: AmpAscoCapConfig( + name=ClassificationName.TIER_2, + direction=Direction.SUPPORTS, + strength=Strength.POTENTIAL, + ), + Classification.TIER_3: AmpAscoCapConfig( + name=ClassificationName.TIER_3, direction=Direction.NEUTRAL, strength=None + ), + Classification.TIER_4: AmpAscoCapConfig( + name=ClassificationName.TIER_4, direction=Direction.DISPUTES, strength=None + ), + } +) class _AmpAscoCapStatement(Statement, ABC): @@ -97,11 +100,16 @@ def _validate_classification_config( classification_code: Classification, classification_name: str, direction: str, - strength_code: str | None, + strength_code: MappableConcept | None, ) -> None: """Validate that classificati""" expected_config = CLASSIFICATION_POLICY_MAP[classification_code] - if strength_code != expected_config.strength: + actual_strength = ( + strength_code.primaryCoding.code.root + if strength_code + else strength_code + ) + if actual_strength != expected_config.strength: expected_strength = ( expected_config.strength.value if expected_config.strength @@ -136,7 +144,7 @@ def _validate_classification_config( Classification(self.classification.primaryCoding.code.root), self.classification.name, self.direction, - self.strength.primaryCoding.code.root, + self.strength, ) # Validate hasEvidenceLines From d58457d3d2c342eb6764fcdd1e3e8917b0b4eac8 Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Mon, 27 Oct 2025 11:36:27 -0400 Subject: [PATCH 03/10] add typing_extensions --- pyproject.toml | 3 ++- src/ga4gh/va_spec/aac_2017/models.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d53ee72..325d29a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,8 @@ dynamic = ["version"] dependencies = [ "ga4gh.vrs>=2.1.3,<3.0", "ga4gh.cat_vrs~=0.7.1", - "pydantic>=2.0,<3.0" + "pydantic>=2.0,<3.0", + "typing_extensions", ] [project.optional-dependencies] diff --git a/src/ga4gh/va_spec/aac_2017/models.py b/src/ga4gh/va_spec/aac_2017/models.py index 78bf63a..def59ed 100644 --- a/src/ga4gh/va_spec/aac_2017/models.py +++ b/src/ga4gh/va_spec/aac_2017/models.py @@ -7,10 +7,10 @@ from abc import ABC from enum import Enum from types import MappingProxyType -from typing import Self from pydantic import Field, model_validator from pydantic.dataclasses import dataclass +from typing_extensions import Self from ga4gh.core.models import MappableConcept, iriReference from ga4gh.va_spec.base.core import ( From 584390d4a628d37beb26415269376bed805e1343 Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Mon, 27 Oct 2025 12:59:11 -0400 Subject: [PATCH 04/10] fix --- src/ga4gh/va_spec/aac_2017/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ga4gh/va_spec/aac_2017/__init__.py b/src/ga4gh/va_spec/aac_2017/__init__.py index 60393d5..37c9b09 100644 --- a/src/ga4gh/va_spec/aac_2017/__init__.py +++ b/src/ga4gh/va_spec/aac_2017/__init__.py @@ -3,6 +3,7 @@ from .models import ( AMP_ASCO_CAP_STRENGTHS, AMP_ASCO_CAP_TIERS, + CLASSIFICATION_POLICY_MAP, Classification, Strength, VariantDiagnosticStatement, @@ -13,6 +14,7 @@ __all__ = [ "AMP_ASCO_CAP_STRENGTHS", "AMP_ASCO_CAP_TIERS", + "CLASSIFICATION_POLICY_MAP", "Classification", "Strength", "VariantDiagnosticStatement", From 1278ccb423ea7568d202e3cb591f0b9e331d06ae Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Mon, 27 Oct 2025 13:32:06 -0400 Subject: [PATCH 05/10] renaming --- src/ga4gh/va_spec/aac_2017/__init__.py | 16 ++++----- src/ga4gh/va_spec/aac_2017/models.py | 47 ++++++++++++++------------ src/ga4gh/va_spec/acmg_2015/models.py | 4 +-- src/ga4gh/va_spec/base/__init__.py | 8 ++--- src/ga4gh/va_spec/base/enums.py | 9 +++-- src/ga4gh/va_spec/ccv_2022/models.py | 4 +-- 6 files changed, 47 insertions(+), 41 deletions(-) diff --git a/src/ga4gh/va_spec/aac_2017/__init__.py b/src/ga4gh/va_spec/aac_2017/__init__.py index 37c9b09..e227922 100644 --- a/src/ga4gh/va_spec/aac_2017/__init__.py +++ b/src/ga4gh/va_spec/aac_2017/__init__.py @@ -1,22 +1,22 @@ """Module to load and init namespace at package level.""" from .models import ( - AMP_ASCO_CAP_STRENGTHS, - AMP_ASCO_CAP_TIERS, + AMP_ASCO_CAP_CLASSIFICATION_CODES, CLASSIFICATION_POLICY_MAP, - Classification, - Strength, + AmpAscoCapClassificationCode, + AmpAscoCapClassificationName, + AsmpAscoCapStrengthCode, VariantDiagnosticStatement, VariantPrognosticStatement, VariantTherapeuticResponseStatement, ) __all__ = [ - "AMP_ASCO_CAP_STRENGTHS", - "AMP_ASCO_CAP_TIERS", + "AMP_ASCO_CAP_CLASSIFICATION_CODES", "CLASSIFICATION_POLICY_MAP", - "Classification", - "Strength", + "AmpAscoCapClassificationCode", + "AmpAscoCapClassificationName", + "AsmpAscoCapStrengthCode", "VariantDiagnosticStatement", "VariantPrognosticStatement", "VariantTherapeuticResponseStatement", diff --git a/src/ga4gh/va_spec/aac_2017/models.py b/src/ga4gh/va_spec/aac_2017/models.py index def59ed..fa9dd05 100644 --- a/src/ga4gh/va_spec/aac_2017/models.py +++ b/src/ga4gh/va_spec/aac_2017/models.py @@ -27,17 +27,14 @@ from ga4gh.va_spec.base.validators import validate_mappable_concept -class Strength(str, Enum): +class AsmpAscoCapStrengthCode(str, Enum): """Define constraints for AMP/ASCO/CAP strength coding""" STRONG = "strong" POTENTIAL = "potential" -AMP_ASCO_CAP_STRENGTHS = [v.value for v in Strength.__members__.values()] - - -class Classification(str, Enum): +class AmpAscoCapClassificationCode(str, Enum): """Define constraints for AMP/ASCO/CAP classification coding""" TIER_1 = "tier 1" @@ -46,10 +43,12 @@ class Classification(str, Enum): TIER_4 = "tier 4" -AMP_ASCO_CAP_TIERS = [v.value for v in Classification.__members__.values()] +AMP_ASCO_CAP_CLASSIFICATION_CODES = [ + v.value for v in AmpAscoCapClassificationCode.__members__.values() +] -class ClassificationName(str, Enum): +class AmpAscoCapClassificationName(str, Enum): """Define constraints for AMP/ASCO/CAP classification name""" TIER_1 = "Tier I" @@ -62,28 +61,32 @@ class ClassificationName(str, Enum): class AmpAscoCapConfig: """AMP/ASCO/CAP config for expected values""" - name: ClassificationName + name: AmpAscoCapClassificationName direction: Direction - strength: Strength | None + strength: AsmpAscoCapStrengthCode | None CLASSIFICATION_POLICY_MAP = MappingProxyType( { - Classification.TIER_1: AmpAscoCapConfig( - name=ClassificationName.TIER_1, + AmpAscoCapClassificationCode.TIER_1: AmpAscoCapConfig( + name=AmpAscoCapClassificationName.TIER_1, direction=Direction.SUPPORTS, - strength=Strength.STRONG, + strength=AsmpAscoCapStrengthCode.STRONG, ), - Classification.TIER_2: AmpAscoCapConfig( - name=ClassificationName.TIER_2, + AmpAscoCapClassificationCode.TIER_2: AmpAscoCapConfig( + name=AmpAscoCapClassificationName.TIER_2, direction=Direction.SUPPORTS, - strength=Strength.POTENTIAL, + strength=AsmpAscoCapStrengthCode.POTENTIAL, ), - Classification.TIER_3: AmpAscoCapConfig( - name=ClassificationName.TIER_3, direction=Direction.NEUTRAL, strength=None + AmpAscoCapClassificationCode.TIER_3: AmpAscoCapConfig( + name=AmpAscoCapClassificationName.TIER_3, + direction=Direction.NEUTRAL, + strength=None, ), - Classification.TIER_4: AmpAscoCapConfig( - name=ClassificationName.TIER_4, direction=Direction.DISPUTES, strength=None + AmpAscoCapClassificationCode.TIER_4: AmpAscoCapConfig( + name=AmpAscoCapClassificationName.TIER_4, + direction=Direction.DISPUTES, + strength=None, ), } ) @@ -97,7 +100,7 @@ def validate_aac_statement(self) -> Self: """Validate AMP/ASCO/CAP statements""" def _validate_classification_config( - classification_code: Classification, + classification_code: AmpAscoCapClassificationCode, classification_name: str, direction: str, strength_code: MappableConcept | None, @@ -137,11 +140,11 @@ def _validate_classification_config( validate_mappable_concept( self.classification, System.AMP_ASCO_CAP, - valid_codes=AMP_ASCO_CAP_TIERS, + valid_codes=AMP_ASCO_CAP_CLASSIFICATION_CODES, mc_is_required=True, ) _validate_classification_config( - Classification(self.classification.primaryCoding.code.root), + AmpAscoCapClassificationCode(self.classification.primaryCoding.code.root), self.classification.name, self.direction, self.strength, diff --git a/src/ga4gh/va_spec/acmg_2015/models.py b/src/ga4gh/va_spec/acmg_2015/models.py index a3e0afa..362645d 100644 --- a/src/ga4gh/va_spec/acmg_2015/models.py +++ b/src/ga4gh/va_spec/acmg_2015/models.py @@ -16,8 +16,8 @@ ) from ga4gh.va_spec.base.enums import ( CLIN_GEN_CLASSIFICATIONS, + STRENGTH_CODES, STRENGTH_OF_EVIDENCE_PROVIDED_VALUES, - STRENGTHS, System, ) from ga4gh.va_spec.base.validators import ( @@ -157,7 +157,7 @@ def validate_strength(cls, v: MappableConcept | None) -> MappableConcept | None: :return: Validated strength value """ return validate_mappable_concept( - v, System.ACMG, valid_codes=STRENGTHS, mc_is_required=False + v, System.ACMG, valid_codes=STRENGTH_CODES, mc_is_required=False ) @field_validator("classification") diff --git a/src/ga4gh/va_spec/base/__init__.py b/src/ga4gh/va_spec/base/__init__.py index 85489ce..187260d 100644 --- a/src/ga4gh/va_spec/base/__init__.py +++ b/src/ga4gh/va_spec/base/__init__.py @@ -31,14 +31,14 @@ from .enums import ( CCV_CLASSIFICATIONS, CLIN_GEN_CLASSIFICATIONS, + STRENGTH_CODES, STRENGTH_OF_EVIDENCE_PROVIDED_VALUES, - STRENGTHS, CcvClassification, ClinGenClassification, DiagnosticPredicate, MembershipOperator, PrognosticPredicate, - Strength, + StrengthCode, StrengthOfEvidenceProvided, System, TherapeuticResponsePredicate, @@ -70,10 +70,10 @@ "Method", "PrognosticPredicate", "Proposition", - "STRENGTHS", + "STRENGTH_CODES", "STRENGTH_OF_EVIDENCE_PROVIDED_VALUES", "Statement", - "Strength", + "StrengthCode", "StrengthOfEvidenceProvided", "StudyGroup", "StudyResult", diff --git a/src/ga4gh/va_spec/base/enums.py b/src/ga4gh/va_spec/base/enums.py index 7615066..aa1413f 100644 --- a/src/ga4gh/va_spec/base/enums.py +++ b/src/ga4gh/va_spec/base/enums.py @@ -52,14 +52,17 @@ class StrengthOfEvidenceProvided(str, Enum): ] -class Strength(str, Enum): - """Define constrains for strength""" +class StrengthCode(str, Enum): + """Define constrains for strength + + Used in ACMG 2015 and CCV 2022 + """ DEFINITIVE = "definitive" LIKELY = "likely" -STRENGTHS = [v.value for v in Strength.__members__.values()] +STRENGTH_CODES = [v.value for v in StrengthCode.__members__.values()] class ClinGenClassification(str, Enum): diff --git a/src/ga4gh/va_spec/ccv_2022/models.py b/src/ga4gh/va_spec/ccv_2022/models.py index 59f0594..a0c20bd 100644 --- a/src/ga4gh/va_spec/ccv_2022/models.py +++ b/src/ga4gh/va_spec/ccv_2022/models.py @@ -16,8 +16,8 @@ ) from ga4gh.va_spec.base.enums import ( CCV_CLASSIFICATIONS, + STRENGTH_CODES, STRENGTH_OF_EVIDENCE_PROVIDED_VALUES, - STRENGTHS, System, ) from ga4gh.va_spec.base.validators import validate_mappable_concept @@ -130,7 +130,7 @@ def validate_strength(cls, v: MappableConcept | None) -> MappableConcept | None: :return: Validated strength value """ return validate_mappable_concept( - v, System.CCV, valid_codes=STRENGTHS, mc_is_required=False + v, System.CCV, valid_codes=STRENGTH_CODES, mc_is_required=False ) @field_validator("classification") From b873ae2be9c97dce4f3b491799e260795e24049d Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Mon, 27 Oct 2025 13:34:34 -0400 Subject: [PATCH 06/10] more rename --- src/ga4gh/va_spec/aac_2017/__init__.py | 4 ++-- src/ga4gh/va_spec/aac_2017/models.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ga4gh/va_spec/aac_2017/__init__.py b/src/ga4gh/va_spec/aac_2017/__init__.py index e227922..db13ca9 100644 --- a/src/ga4gh/va_spec/aac_2017/__init__.py +++ b/src/ga4gh/va_spec/aac_2017/__init__.py @@ -2,7 +2,7 @@ from .models import ( AMP_ASCO_CAP_CLASSIFICATION_CODES, - CLASSIFICATION_POLICY_MAP, + AMP_ASCO_CAP_CLASSIFICATION_MAP, AmpAscoCapClassificationCode, AmpAscoCapClassificationName, AsmpAscoCapStrengthCode, @@ -13,7 +13,7 @@ __all__ = [ "AMP_ASCO_CAP_CLASSIFICATION_CODES", - "CLASSIFICATION_POLICY_MAP", + "AMP_ASCO_CAP_CLASSIFICATION_MAP", "AmpAscoCapClassificationCode", "AmpAscoCapClassificationName", "AsmpAscoCapStrengthCode", diff --git a/src/ga4gh/va_spec/aac_2017/models.py b/src/ga4gh/va_spec/aac_2017/models.py index fa9dd05..97701eb 100644 --- a/src/ga4gh/va_spec/aac_2017/models.py +++ b/src/ga4gh/va_spec/aac_2017/models.py @@ -66,7 +66,7 @@ class AmpAscoCapConfig: strength: AsmpAscoCapStrengthCode | None -CLASSIFICATION_POLICY_MAP = MappingProxyType( +AMP_ASCO_CAP_CLASSIFICATION_MAP = MappingProxyType( { AmpAscoCapClassificationCode.TIER_1: AmpAscoCapConfig( name=AmpAscoCapClassificationName.TIER_1, @@ -106,7 +106,7 @@ def _validate_classification_config( strength_code: MappableConcept | None, ) -> None: """Validate that classificati""" - expected_config = CLASSIFICATION_POLICY_MAP[classification_code] + expected_config = AMP_ASCO_CAP_CLASSIFICATION_MAP[classification_code] actual_strength = ( strength_code.primaryCoding.code.root if strength_code From e64fbf8a2a1d9c77ed1460e89f10db3e455988a8 Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Wed, 29 Oct 2025 07:41:07 -0400 Subject: [PATCH 07/10] apply system + evidence line strength updates --- src/ga4gh/va_spec/aac_2017/__init__.py | 4 ++++ src/ga4gh/va_spec/aac_2017/models.py | 21 +++++++++++++++++++++ src/ga4gh/va_spec/base/enums.py | 2 +- submodules/va_spec | 2 +- tests/validation/test_va_spec_models.py | 23 +++++++++++++++-------- 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/ga4gh/va_spec/aac_2017/__init__.py b/src/ga4gh/va_spec/aac_2017/__init__.py index db13ca9..376df5a 100644 --- a/src/ga4gh/va_spec/aac_2017/__init__.py +++ b/src/ga4gh/va_spec/aac_2017/__init__.py @@ -3,8 +3,10 @@ from .models import ( AMP_ASCO_CAP_CLASSIFICATION_CODES, AMP_ASCO_CAP_CLASSIFICATION_MAP, + AMP_ASCO_CAP_EVIDENCE_LINE_STRENGTHS, AmpAscoCapClassificationCode, AmpAscoCapClassificationName, + AmpAscoCapEvidenceLineStrength, AsmpAscoCapStrengthCode, VariantDiagnosticStatement, VariantPrognosticStatement, @@ -14,8 +16,10 @@ __all__ = [ "AMP_ASCO_CAP_CLASSIFICATION_CODES", "AMP_ASCO_CAP_CLASSIFICATION_MAP", + "AMP_ASCO_CAP_EVIDENCE_LINE_STRENGTHS", "AmpAscoCapClassificationCode", "AmpAscoCapClassificationName", + "AmpAscoCapEvidenceLineStrength", "AsmpAscoCapStrengthCode", "VariantDiagnosticStatement", "VariantPrognosticStatement", diff --git a/src/ga4gh/va_spec/aac_2017/models.py b/src/ga4gh/va_spec/aac_2017/models.py index 97701eb..b18e138 100644 --- a/src/ga4gh/va_spec/aac_2017/models.py +++ b/src/ga4gh/va_spec/aac_2017/models.py @@ -57,6 +57,20 @@ class AmpAscoCapClassificationName(str, Enum): TIER_4 = "Tier IV" +class AmpAscoCapEvidenceLineStrength(str, Enum): + """Define constraints for AMP/ASCO/CAP `EvidenceLine.strengthOfEvidenceProvided`""" + + A = "A" + B = "B" + C = "C" + D = "D" + + +AMP_ASCO_CAP_EVIDENCE_LINE_STRENGTHS = [ + v.value for v in AmpAscoCapEvidenceLineStrength.__members__.values() +] + + @dataclass class AmpAscoCapConfig: """AMP/ASCO/CAP config for expected values""" @@ -163,6 +177,13 @@ def _validate_classification_config( msg = "`targetProposition` must be one of: `VariantDiagnosticProposition`, `VariantPrognosticProposition`, `VariantTherapeuticResponseProposition`" raise ValueError(msg) + validate_mappable_concept( + evidence_line.strengthOfEvidenceProvided, + System.AMP_ASCO_CAP, + valid_codes=AMP_ASCO_CAP_EVIDENCE_LINE_STRENGTHS, + mc_is_required=False, + ) + return self diff --git a/src/ga4gh/va_spec/base/enums.py b/src/ga4gh/va_spec/base/enums.py index aa1413f..5f3f684 100644 --- a/src/ga4gh/va_spec/base/enums.py +++ b/src/ga4gh/va_spec/base/enums.py @@ -95,6 +95,6 @@ class System(str, Enum): """Define constraints for systems""" ACMG = "ACMG Guidelines, 2015" - AMP_ASCO_CAP = "AMP/ASCO/CAP (AAC) Guidelines, 2017" + AMP_ASCO_CAP = "AMP/ASCO/CAP Guidelines, 2017" CLIN_GEN = "ClinGen Low Penetrance and Risk Allele Recommendations, 2024" CCV = "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022" diff --git a/submodules/va_spec b/submodules/va_spec index f2379c7..27f76a2 160000 --- a/submodules/va_spec +++ b/submodules/va_spec @@ -1 +1 @@ -Subproject commit f2379c75dc1af9eb06cdd60aa001266f3e2872d7 +Subproject commit 27f76a2da27bbfd36e9d617c3996317ebecfa94f diff --git a/tests/validation/test_va_spec_models.py b/tests/validation/test_va_spec_models.py index 1a111f3..2c4461a 100644 --- a/tests/validation/test_va_spec_models.py +++ b/tests/validation/test_va_spec_models.py @@ -239,13 +239,13 @@ def test_evidence_line(caf): }, "strength": { "primaryCoding": { - "system": "AMP/ASCO/CAP (AAC) Guidelines, 2017", + "system": "AMP/ASCO/CAP Guidelines, 2017", "code": "strong", } }, "classification": { "primaryCoding": { - "system": "AMP/ASCO/CAP (AAC) Guidelines, 2017", + "system": "AMP/ASCO/CAP Guidelines, 2017", "code": "tier 1", } }, @@ -352,7 +352,7 @@ def test_variant_pathogenicity_stmt(): invalid_params = deepcopy(params) invalid_params["classification"]["primaryCoding"]["system"] = ( - "AMP/ASCO/CAP (AAC) Guidelines, 2017" + "AMP/ASCO/CAP Guidelines, 2017" ) with pytest.raises(ValueError, match="`primaryCoding.system` must be one of"): VariantPathogenicityStatement(**invalid_params) @@ -459,7 +459,7 @@ def test_variant_pathogenicity_el(): invalid_params = deepcopy(params) invalid_params["strengthOfEvidenceProvided"] = { "primaryCoding": { - "system": "AMP/ASCO/CAP (AAC) Guidelines, 2017", + "system": "AMP/ASCO/CAP Guidelines, 2017", "code": "strong", } } @@ -624,7 +624,7 @@ def test_aac_statement(): "strength": { "primaryCoding": { "code": "strong", - "system": "AMP/ASCO/CAP (AAC) Guidelines, 2017", + "system": "AMP/ASCO/CAP Guidelines, 2017", } }, "specifiedBy": "documents.json#/1", @@ -632,7 +632,7 @@ def test_aac_statement(): "name": "Tier I", "primaryCoding": { "code": "tier 1", - "system": "AMP/ASCO/CAP (AAC) Guidelines, 2017", + "system": "AMP/ASCO/CAP Guidelines, 2017", }, }, "hasEvidenceLines": [ @@ -640,8 +640,8 @@ def test_aac_statement(): "directionOfEvidenceProvided": "supports", "strengthOfEvidenceProvided": { "primaryCoding": { - "code": "Level A", - "system": "AMP/ASCO/CAP (AAC) Guidelines, 2017", + "code": "A", + "system": "AMP/ASCO/CAP Guidelines, 2017", } }, "hasEvidenceItems": [ @@ -669,6 +669,13 @@ def test_aac_statement(): } assert VariantTherapeuticResponseStatement(**params) + # No strengthOfEvidenceProvided + no_evidence_line_strength_params = deepcopy(params) + no_evidence_line_strength_params["hasEvidenceLines"][0].pop( + "strengthOfEvidenceProvided" + ) + assert VariantTherapeuticResponseStatement(**no_evidence_line_strength_params) + # Invalid strength invalid_params = deepcopy(params) invalid_params["strength"]["primaryCoding"]["code"] = "Strong" From 1c56c17be880dd3780f63859d898fb7f7128edb7 Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Fri, 28 Nov 2025 08:19:01 -0500 Subject: [PATCH 08/10] apply VariantClinicalSignificanceStatement + evidence line changes --- .gitmodules | 2 +- src/ga4gh/va_spec/aac_2017/__init__.py | 16 +- src/ga4gh/va_spec/aac_2017/models.py | 216 ++++++++++++------------ src/ga4gh/va_spec/base/core.py | 4 +- submodules/va_spec | 2 +- tests/validation/test_va_spec_models.py | 40 ++--- 6 files changed, 139 insertions(+), 141 deletions(-) diff --git a/.gitmodules b/.gitmodules index 64cd7b9..c464d0b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "submodules/va_spec"] path = submodules/va_spec url = https://github.com/ga4gh/va-spec - branch = issue-293 + branch = fix-aac diff --git a/src/ga4gh/va_spec/aac_2017/__init__.py b/src/ga4gh/va_spec/aac_2017/__init__.py index 376df5a..0f0f8f2 100644 --- a/src/ga4gh/va_spec/aac_2017/__init__.py +++ b/src/ga4gh/va_spec/aac_2017/__init__.py @@ -6,11 +6,13 @@ AMP_ASCO_CAP_EVIDENCE_LINE_STRENGTHS, AmpAscoCapClassificationCode, AmpAscoCapClassificationName, + AmpAscoCapEvidenceLine, AmpAscoCapEvidenceLineStrength, AsmpAscoCapStrengthCode, - VariantDiagnosticStatement, - VariantPrognosticStatement, - VariantTherapeuticResponseStatement, + DiagnosticEvidenceLine, + PrognosticEvidenceLine, + TherapeuticEvidenceLine, + VariantClinicalSignificanceStatement, ) __all__ = [ @@ -19,9 +21,11 @@ "AMP_ASCO_CAP_EVIDENCE_LINE_STRENGTHS", "AmpAscoCapClassificationCode", "AmpAscoCapClassificationName", + "AmpAscoCapEvidenceLine", "AmpAscoCapEvidenceLineStrength", "AsmpAscoCapStrengthCode", - "VariantDiagnosticStatement", - "VariantPrognosticStatement", - "VariantTherapeuticResponseStatement", + "DiagnosticEvidenceLine", + "PrognosticEvidenceLine", + "TherapeuticEvidenceLine", + "VariantClinicalSignificanceStatement", ] diff --git a/src/ga4gh/va_spec/aac_2017/models.py b/src/ga4gh/va_spec/aac_2017/models.py index b18e138..f9cf6a2 100644 --- a/src/ga4gh/va_spec/aac_2017/models.py +++ b/src/ga4gh/va_spec/aac_2017/models.py @@ -4,11 +4,10 @@ sequence variants in cancer. """ -from abc import ABC from enum import Enum from types import MappingProxyType -from pydantic import Field, model_validator +from pydantic import Field, field_validator, model_validator from pydantic.dataclasses import dataclass from typing_extensions import Self @@ -27,6 +26,57 @@ from ga4gh.va_spec.base.validators import validate_mappable_concept +class AmpAscoCapEvidenceLineStrength(str, Enum): + """Define constraints for AMP/ASCO/CAP `EvidenceLine.strengthOfEvidenceProvided`""" + + A = "A" + B = "B" + C = "C" + D = "D" + + +class AmpAscoCapEvidenceLine(EvidenceLine): + """Evidence line for AMP/ASCO/CAP""" + + targetProposition: ( + VariantDiagnosticProposition + | VariantPrognosticProposition + | VariantTherapeuticResponseProposition + ) + + @field_validator("strengthOfEvidenceProvided", mode="after") + @classmethod + def validate_strength_of_evidence_provided( + cls, v: MappableConcept | None + ) -> MappableConcept | None: + """Validate strengthOfEvidenceProvided""" + validate_mappable_concept( + v, + System.AMP_ASCO_CAP, + valid_codes=AMP_ASCO_CAP_EVIDENCE_LINE_STRENGTHS, + mc_is_required=False, + ) + return v + + +class DiagnosticEvidenceLine(AmpAscoCapEvidenceLine): + """Diagnostic evidence line for AMP/ASCO/CAP""" + + targetProposition: VariantDiagnosticProposition + + +class PrognosticEvidenceLine(AmpAscoCapEvidenceLine): + """Prognostic evidence line for AMP/ASCO/CAP""" + + targetProposition: VariantPrognosticProposition + + +class TherapeuticEvidenceLine(AmpAscoCapEvidenceLine): + """Therapeutic evidence line for AMP/ASCO/CAP""" + + targetProposition: VariantTherapeuticResponseProposition + + class AsmpAscoCapStrengthCode(str, Enum): """Define constraints for AMP/ASCO/CAP strength coding""" @@ -37,10 +87,10 @@ class AsmpAscoCapStrengthCode(str, Enum): class AmpAscoCapClassificationCode(str, Enum): """Define constraints for AMP/ASCO/CAP classification coding""" - TIER_1 = "tier 1" - TIER_2 = "tier 2" - TIER_3 = "tier 3" - TIER_4 = "tier 4" + TIER_1 = "tier i" + TIER_2 = "tier ii" + TIER_3 = "tier iii" + TIER_4 = "tier iv" AMP_ASCO_CAP_CLASSIFICATION_CODES = [ @@ -57,15 +107,6 @@ class AmpAscoCapClassificationName(str, Enum): TIER_4 = "Tier IV" -class AmpAscoCapEvidenceLineStrength(str, Enum): - """Define constraints for AMP/ASCO/CAP `EvidenceLine.strengthOfEvidenceProvided`""" - - A = "A" - B = "B" - C = "C" - D = "D" - - AMP_ASCO_CAP_EVIDENCE_LINE_STRENGTHS = [ v.value for v in AmpAscoCapEvidenceLineStrength.__members__.values() ] @@ -106,20 +147,39 @@ class AmpAscoCapConfig: ) -class _AmpAscoCapStatement(Statement, ABC): - """Abstract base class for AAC 2017 statements""" +class VariantClinicalSignificanceStatement(Statement): + """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 + results. + """ + + proposition: VariantClinicalSignificanceProposition = Field( + ..., + description="A proposition about a diagnostic association between a variant and condition, for which the study provides evidence. The validity of this proposition, and the level of confidence/evidence supporting it, may be assessed and reported by the Statement.", + ) + strength: MappableConcept | None = Field( + default=None, + description="The strength of support that the Statement is determined to provide for or against the Diagnostic Proposition for the assessed variant, based on the curation and reporting conventions of the AMP/ASCO/CAP 2017 Guidelines.", + ) + classification: MappableConcept = 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.", + ) + specifiedBy: Method | iriReference @model_validator(mode="after") def validate_aac_statement(self) -> Self: - """Validate AMP/ASCO/CAP statements""" + """Validate VariantClinicalSignificanceStatement""" def _validate_classification_config( classification_code: AmpAscoCapClassificationCode, classification_name: str, direction: str, strength_code: MappableConcept | None, + has_evidence_lines: list, ) -> None: - """Validate that classificati""" + """Validate that classification config is correct""" expected_config = AMP_ASCO_CAP_CLASSIFICATION_MAP[classification_code] actual_strength = ( strength_code.primaryCoding.code.root @@ -143,6 +203,30 @@ def _validate_classification_config( msg = f"`direction` must be: {expected_config.direction.value}" raise ValueError(msg) + if classification_code in { + AmpAscoCapClassificationCode.TIER_1, + AmpAscoCapClassificationCode.TIER_2, + }: + for evidence_line in has_evidence_lines: + found_approved_el_clas = False + for approved_el_cls in [ + DiagnosticEvidenceLine, + PrognosticEvidenceLine, + TherapeuticEvidenceLine, + iriReference, + ]: + try: + approved_el_cls(**evidence_line.model_dump()) + except Exception: # noqa: S110 + pass + else: + found_approved_el_clas = True + break + + if not found_approved_el_clas: + msg = "`hasEvidenceLines` must be one of: `DiagnosticEvidenceLine`, `PrognosticEvidenceLine`, `TherapeuticEvidenceLine`, or `iriReference`" + raise ValueError(msg) + # Validate strength validate_mappable_concept( self.strength, @@ -162,99 +246,7 @@ def _validate_classification_config( self.classification.name, self.direction, self.strength, + self.hasEvidenceLines or [], ) - # Validate hasEvidenceLines - for evidence_line in self.hasEvidenceLines or []: - if isinstance(evidence_line, EvidenceLine): - target_proposition = evidence_line.targetProposition - if target_proposition and not isinstance( - target_proposition, - VariantDiagnosticProposition - | VariantPrognosticProposition - | VariantTherapeuticResponseProposition, - ): - msg = "`targetProposition` must be one of: `VariantDiagnosticProposition`, `VariantPrognosticProposition`, `VariantTherapeuticResponseProposition`" - raise ValueError(msg) - - validate_mappable_concept( - evidence_line.strengthOfEvidenceProvided, - System.AMP_ASCO_CAP, - valid_codes=AMP_ASCO_CAP_EVIDENCE_LINE_STRENGTHS, - mc_is_required=False, - ) - return self - - -class VariantDiagnosticStatement(_AmpAscoCapStatement): - """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 - results. - """ - - proposition: VariantClinicalSignificanceProposition = Field( - ..., - description="A proposition about a diagnostic association between a variant and condition, for which the study provides evidence. The validity of this proposition, and the level of confidence/evidence supporting it, may be assessed and reported by the Statement.", - ) - strength: MappableConcept | None = Field( - default=None, - description="The strength of support that the Statement is determined to provide for or against the Diagnostic Proposition for the assessed variant, based on the curation and reporting conventions of the AMP/ASCO/CAP (AAC) 2017 Guidelines.", - ) - classification: MappableConcept = 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 - reported here using terms from the AMP/ASCO/CAP (AAC) 2017 Guidelines.", - ) - specifiedBy: Method | iriReference = Field( - ..., - description="A method that specifies how the diagnostic classification was ultimately assigned to the variant, based on assessment of evidence.", - ) - - -class VariantPrognosticStatement(_AmpAscoCapStatement): - """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. - """ - - proposition: VariantClinicalSignificanceProposition = Field( - ..., - description="A proposition about a prognostic association between a variant and condition, for which the study provides evidence. The validity of this proposition, and the level of confidence/evidence supporting it, may be assessed and reported by the Statement.", - ) - strength: MappableConcept | None = Field( - default=None, - description="The strength of support that the Statement is determined to provide for or against the Prognostic Proposition for the assessed variant, based on the curation and reporting conventions of the AMP/ASCO/CAP (AAC) 2017 Guidelines.", - ) - classification: MappableConcept = 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 - reported here using terms from the AMP/ASCO/CAP (AAC) 2017 Guidelines. Note that the enumerated value set here is bound to the `code` field of the Coding object that is nested inside a MappableConcept's primary coding.", - ) - specifiedBy: Method | iriReference = Field( - ..., - description="A method that specifies how the prognostic classification was ultimately assigned to the variant, based on assessment of evidence.", - ) - - -class VariantTherapeuticResponseStatement(_AmpAscoCapStatement): - """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. - """ - - proposition: VariantClinicalSignificanceProposition = Field( - ..., - description="A proposition about the therapeutic response associated with a variant, for which the study provides evidence. The validity of this proposition, and the level of confidence/evidence supporting it, may be assessed and reported by the Statement.", - ) - strength: MappableConcept | None = Field( - default=None, - description="The strength of support that the Statement is determined to provide for or against the Therapeutic Response Proposition for the assessed variant, based on the curation and reporting conventions of the AMP/ASCO/CAP (AAC) 2017 Guidelines.", - ) - classification: MappableConcept = 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 - reported here using terms from the AMP/ASCO/CAP (AAC) 2017 Guidelines.", - ) - specifiedBy: Method | iriReference = Field( - ..., - description="A method that specifies how the therapeutic response classification was ultimately assigned to the variant, based on assessment of evidence.", - ) diff --git a/src/ga4gh/va_spec/base/core.py b/src/ga4gh/va_spec/base/core.py index 3062bb7..3b00ad3 100644 --- a/src/ga4gh/va_spec/base/core.py +++ b/src/ga4gh/va_spec/base/core.py @@ -392,7 +392,7 @@ class ClinicalVariantProposition(_SubjectVariantPropositionBase): ) alleleOriginQualifier: MappableConcept | iriReference | None = Field( default=None, - description="Reports whether the Proposition should be interpreted in the context of a heritable 'germline' variant, an acquired 'somatic' variant in a tumor, post-zygotic 'mosaic' variant. While these are the most commonly reported allele origins, other more nuanced concepts can be captured (e.g. 'maternal' vs 'paternal' allele origin'). In practice, populating this field may be complicated by the fact that some sources report allele origin based on the type of tissue that was sequenced to identify the variant, and others use it more generally to specify a category of variant for which the proposition holds. The stated intent of this attribute is the latter. However, if an implementer is not sure about which is reported in their data, it may be safer to create an Extension to hold this information, where they can explicitly acknowledge this ambiguity.", + description='Reports whether the Proposition should be interpreted in the context of a heritable "germline" variant, an acquired "somatic" variant in a tumor, or a post-zygotic "mosaic" variant. While these are the most commonly reported allele origins, other more nuanced concepts can be captured (e.g. "maternal" vs "paternal" allele origin). In practice, populating this field may be complicated by the fact that some sources report allele origin based on the type of tissue that was sequenced to identify the variant, and others use it more generally to specify a category of variant for which the proposition holds. The stated intent of this attribute is the latter. However, if an implementer is not sure about which is reported in their data, it may be safer to create an Extension to hold this information, where they can explicitly acknowledge this ambiguity.', ) @@ -438,7 +438,7 @@ class VariantClinicalSignificanceProposition( description="The predicate associating the subject variant to clinical significance for the object Condition. MUST be 'hasClinicalSignificanceFor'.", ) objectCondition: Condition | iriReference = Field( - ..., description="The disease that is evaluated." + ..., description="The condition that is evaluated." ) diff --git a/submodules/va_spec b/submodules/va_spec index 27f76a2..e9c8117 160000 --- a/submodules/va_spec +++ b/submodules/va_spec @@ -1 +1 @@ -Subproject commit 27f76a2da27bbfd36e9d617c3996317ebecfa94f +Subproject commit e9c81175a88bb7064756093a7e9b663f1f0aca07 diff --git a/tests/validation/test_va_spec_models.py b/tests/validation/test_va_spec_models.py index 2c4461a..4fa7e85 100644 --- a/tests/validation/test_va_spec_models.py +++ b/tests/validation/test_va_spec_models.py @@ -10,7 +10,7 @@ from ga4gh.core.models import Coding, MappableConcept, code, iriReference from ga4gh.va_spec import acmg_2015, base, ccv_2022 -from ga4gh.va_spec.aac_2017.models import VariantTherapeuticResponseStatement +from ga4gh.va_spec.aac_2017.models import VariantClinicalSignificanceStatement from ga4gh.va_spec.acmg_2015.models import ( VariantPathogenicityEvidenceLine, VariantPathogenicityStatement, @@ -246,7 +246,7 @@ def test_evidence_line(caf): "classification": { "primaryCoding": { "system": "AMP/ASCO/CAP Guidelines, 2017", - "code": "tier 1", + "code": "tier i", } }, "specifiedBy": { @@ -613,6 +613,12 @@ def test_variant_onco_el(): def test_aac_statement(): """Test that AMP/ASCO/CAP statement model validators work correctly""" + prop = { + "type": "VariantDiagnosticProposition", + "predicate": "isDiagnosticExclusionCriterionFor", + "objectCondition": "conditions.json#/1", + "subjectVariant": "alleles.json#/1", + } params = { "direction": "supports", "proposition": { @@ -631,12 +637,13 @@ def test_aac_statement(): "classification": { "name": "Tier I", "primaryCoding": { - "code": "tier 1", + "code": "tier i", "system": "AMP/ASCO/CAP Guidelines, 2017", }, }, "hasEvidenceLines": [ { + "targetProposition": prop, "directionOfEvidenceProvided": "supports", "strengthOfEvidenceProvided": { "primaryCoding": { @@ -649,12 +656,7 @@ def test_aac_statement(): { "type": "Statement", "direction": "supports", - "proposition": { - "type": "VariantDiagnosticProposition", - "predicate": "isDiagnosticExclusionCriterionFor", - "objectCondition": "conditions.json#/1", - "subjectVariant": "alleles.json#/1", - }, + "proposition": prop, "strength": { "primaryCoding": { "code": "A", @@ -667,50 +669,50 @@ def test_aac_statement(): }, ], } - assert VariantTherapeuticResponseStatement(**params) + assert VariantClinicalSignificanceStatement(**params) # No strengthOfEvidenceProvided no_evidence_line_strength_params = deepcopy(params) no_evidence_line_strength_params["hasEvidenceLines"][0].pop( "strengthOfEvidenceProvided" ) - assert VariantTherapeuticResponseStatement(**no_evidence_line_strength_params) + assert VariantClinicalSignificanceStatement(**no_evidence_line_strength_params) # Invalid strength invalid_params = deepcopy(params) invalid_params["strength"]["primaryCoding"]["code"] = "Strong" with pytest.raises(ValidationError, match="`strength` must be: strong"): - VariantTherapeuticResponseStatement(**invalid_params) + VariantClinicalSignificanceStatement(**invalid_params) invalid_params = deepcopy(params) invalid_params["strength"]["primaryCoding"]["code"] = "potential" with pytest.raises(ValidationError, match="`strength` must be: strong"): - VariantTherapeuticResponseStatement(**invalid_params) + VariantClinicalSignificanceStatement(**invalid_params) # Invalid classification invalid_params = deepcopy(params) invalid_params["classification"]["primaryCoding"]["code"] = "Tier I" with pytest.raises(ValidationError, match="`primaryCoding.code` must be one of"): - VariantTherapeuticResponseStatement(**invalid_params) + VariantClinicalSignificanceStatement(**invalid_params) invalid_params = deepcopy(params) - invalid_params["classification"]["name"] = "tier 1" + invalid_params["classification"]["name"] = "tier i" with pytest.raises(ValidationError, match="`classification.name` must be: Tier I"): - VariantTherapeuticResponseStatement(**invalid_params) + VariantClinicalSignificanceStatement(**invalid_params) # Invalid direction invalid_params = deepcopy(params) invalid_params["direction"] = "disputes" with pytest.raises(ValidationError, match="`direction` must be: supports"): - VariantTherapeuticResponseStatement(**invalid_params) + VariantClinicalSignificanceStatement(**invalid_params) # Invalid targetProposition invalid_params = deepcopy(params) invalid_params["hasEvidenceLines"][0]["targetProposition"] = invalid_params[ "proposition" ] - with pytest.raises(ValidationError, match="`targetProposition` must be one of"): - VariantTherapeuticResponseStatement(**invalid_params) + with pytest.raises(ValidationError, match="`hasEvidenceLines` must be one of"): + VariantClinicalSignificanceStatement(**invalid_params) def test_examples(test_definitions): From 87d00242c653f3ff91c80d427e75f493953356bf Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Mon, 16 Feb 2026 10:03:35 -0500 Subject: [PATCH 09/10] fixes --- .gitmodules | 2 +- src/ga4gh/va_spec/base/core.py | 4 ++++ submodules/va_spec | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index c464d0b..b1735c4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "submodules/va_spec"] path = submodules/va_spec url = https://github.com/ga4gh/va-spec - branch = fix-aac + branch = v1 diff --git a/src/ga4gh/va_spec/base/core.py b/src/ga4gh/va_spec/base/core.py index ba54151..1f9c622 100644 --- a/src/ga4gh/va_spec/base/core.py +++ b/src/ga4gh/va_spec/base/core.py @@ -719,3 +719,7 @@ class Statement(InformationEntity, BaseModelForbidExtra): default=None, description="An evidence-based argument that supports or disputes the validity of the proposition that a Statement assesses or puts forth as true. The strength and direction of this argument (whether it supports or disputes the proposition, and how strongly) is based on an interpretation of one or more pieces of information as evidence (i.e. 'Evidence Items).", ) + + +Statement.model_rebuild() +EvidenceLine.model_rebuild() diff --git a/submodules/va_spec b/submodules/va_spec index e9c8117..7caecd4 160000 --- a/submodules/va_spec +++ b/submodules/va_spec @@ -1 +1 @@ -Subproject commit e9c81175a88bb7064756093a7e9b663f1f0aca07 +Subproject commit 7caecd474864b0e4bcb7764b545c0fa352ee8798 From 8225abefe9451c21afd1de4701fa4035760d54c7 Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Mon, 16 Feb 2026 12:18:04 -0500 Subject: [PATCH 10/10] fix copy/paste --- src/ga4gh/va_spec/base/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ga4gh/va_spec/base/core.py b/src/ga4gh/va_spec/base/core.py index 1f9c622..3a4b402 100644 --- a/src/ga4gh/va_spec/base/core.py +++ b/src/ga4gh/va_spec/base/core.py @@ -373,7 +373,7 @@ class SubjectVariantProposition(RootModel): | VariantPrognosticProposition | VariantOncogenicityProposition | VariantTherapeuticResponseProposition - | VariantTherapeuticResponseProposition + | VariantClinicalSignificanceProposition ) = Field(discriminator="type")