diff --git a/civicpy/civic.py b/civicpy/civic.py index 45bbb95..a9cca70 100644 --- a/civicpy/civic.py +++ b/civicpy/civic.py @@ -468,9 +468,9 @@ def _is_valid_for_gks_json(cls, emit_warnings: bool = False) -> bool: warnings.append(f"{prefix} does not have 'accepted' status. Skipping") record_type = cls.evidence_type if isinstance(cls, Evidence) else cls.assertion_type - if record_type not in ("DIAGNOSTIC", "PREDICTIVE", "PROGNOSTIC"): + if record_type not in ("DIAGNOSTIC", "PREDICTIVE", "PROGNOSTIC", "ONCOGENIC"): warnings.append( - f"{prefix} type is not one of: 'DIAGNOSTIC', 'PREDICTIVE', or 'PROGNOSTIC'. Skipping" + f"{prefix} type is not one of: 'DIAGNOSTIC', 'PREDICTIVE', 'PROGNOSTIC', or 'ONCOGENIC'. Skipping" ) len_mp_variants = len(cls.molecular_profile.variants) diff --git a/civicpy/cli.py b/civicpy/cli.py index 027314b..f4499da 100644 --- a/civicpy/cli.py +++ b/civicpy/cli.py @@ -5,6 +5,7 @@ from civicpy.__env__ import LOCAL_CACHE_PATH from civicpy.exports.civic_gks_record import ( CivicGksRecordError, + CivicGksOncogenicAssertion, CivicGksClinSigAssertion, ClinVarSubmissionType, create_gks_record_from_assertion, @@ -101,7 +102,13 @@ def create_gks_json( """Create a JSON file for CIViC assertion records approved by a specific organization that are ready for ClinVar submission, represented as GKS objects. For now, we will only support simple molecular profiles and diagnostic, prognostic, - or predictive assertions. + predictive, or oncogenic assertions. + + ClinVar only supports submitting records of the same submission type for a + given assertion criteria: + * Clinical Impact -> diagnostic, prognostic, or predictive assertion + * Oncogenicity -> oncogenic assertion + Therefore, you must create separate GKS JSON for each submission type ClinVar only supports submitting records of the same submission type for a given assertion criteria: @@ -118,7 +125,7 @@ def create_gks_json( logging.exception("Error getting organization %i", organization_id) return - records: list[CivicGksClinSigAssertion] = [] + records: list[CivicGksClinSigAssertion] | list[CivicGksOncogenicAssertion] = [] errors: list[GksAssertionError] = [] for approval in civic.get_all_approvals_ready_for_clinvar_submission_for_org( diff --git a/civicpy/exports/civic_gks_record.py b/civicpy/exports/civic_gks_record.py index a0b285b..bfd9278 100644 --- a/civicpy/exports/civic_gks_record.py +++ b/civicpy/exports/civic_gks_record.py @@ -2,6 +2,8 @@ * CIViC Predictive, Prognostic, and Diagnostic Assertions map to Variant Clinical Significance Statements that follow the AMP/ASCO/CAP 2017 guidelines +* CIViC Oncogenic Assertions map to Variant Oncogenicity Statements that follow + the ClinGen/CGC/VICC Oncogenicity 2022 guidelines """ import logging @@ -17,6 +19,7 @@ MappableConcept, MembershipOperator, Relation, + code, iriReference, ) from ga4gh.va_spec.aac_2017 import ( @@ -30,6 +33,7 @@ ) from ga4gh.va_spec.base import ( Agent, + CcvClassification, ConditionSet, Contribution, DiagnosticPredicate, @@ -38,14 +42,24 @@ Method, PrognosticPredicate, Statement, + StrengthCode, System, TherapeuticResponsePredicate, TherapyGroup, VariantClinicalSignificanceProposition, VariantDiagnosticProposition, + VariantOncogenicityProposition, VariantPrognosticProposition, VariantTherapeuticResponseProposition, ) +from ga4gh.va_spec.ccv_2022 import ( + METHOD as CCV_METHOD, +) +from ga4gh.va_spec.ccv_2022 import ( + VariantOncogenicityEvidenceLine, + VariantOncogenicityStatement, +) +from ga4gh.va_spec.ccv_2022.derived_evidence import derive_onco_evidence_attributes from ga4gh.vrs.models import Expression, Syntax from pydantic import BaseModel @@ -95,6 +109,26 @@ class CivicEvidenceAssertionType(str, Enum): PREDICTIVE = "PREDICTIVE" PROGNOSTIC = "PROGNOSTIC" DIAGNOSTIC = "DIAGNOSTIC" + ONCOGENIC = "ONCOGENIC" + + +class CivicSignificance(str, Enum): + """Define constraints for significance values + + Not exhaustive. Only supports those that can be represented by GKS. + """ + + BENIGN = "BENIGN" + BETTER_OUTCOME = "BETTER_OUTCOME" + LIKELY_BENIGN = "LIKELY_BENIGN" + LIKELY_ONCOGENIC = "LIKELY_ONCOGENIC" + ONCOGENIC = "ONCOGENIC" + POOR_OUTCOME = "POOR_OUTCOME" + POSITIVE = "POSITIVE" + NEGATIVE = "NEGATIVE" + RESISTANCE = "RESISTANCE" + SENSITIVITY_RESPONSE = "SENSITIVITYRESPONSE" + UNCERTAIN_SIGNIFICANCE = "UNCERTAIN_SIGNIFICANCE" CLINICAL_SIGNIFICANCE_ASSERTION_TYPES = [ @@ -102,17 +136,20 @@ class CivicEvidenceAssertionType(str, Enum): CivicEvidenceAssertionType.PROGNOSTIC.value, CivicEvidenceAssertionType.DIAGNOSTIC.value, ] +ONCOGENIC_ASSERTION_TYPES = [CivicEvidenceAssertionType.ONCOGENIC.value] class ClinVarSubmissionType(str, Enum): """Define supported submission types to ClinVar""" CLINICAL_IMPACT = "clinical_impact" + ONCOGENICITY = "oncogenicity" ASSERTION_TYPES_BY_CLINVAR_SUBMISSION_TYPE = MappingProxyType( { ClinVarSubmissionType.CLINICAL_IMPACT: CLINICAL_SIGNIFICANCE_ASSERTION_TYPES, + ClinVarSubmissionType.ONCOGENICITY: ONCOGENIC_ASSERTION_TYPES, } ) @@ -148,16 +185,21 @@ class CivicEvidenceName(str, Enum): } ) - +_IS_ONCOGENIC_FOR_PREDICATE = "isOncogenicFor" # CIViC significance to GKS predicate CLIN_SIG_TO_PREDICATE = MappingProxyType( { - "SENSITIVITYRESPONSE": TherapeuticResponsePredicate.SENSITIVITY, - "RESISTANCE": TherapeuticResponsePredicate.RESISTANCE, - "POOR_OUTCOME": PrognosticPredicate.WORSE_OUTCOME, - "BETTER_OUTCOME": PrognosticPredicate.BETTER_OUTCOME, - "POSITIVE": DiagnosticPredicate.INCLUSIVE, - "NEGATIVE": DiagnosticPredicate.EXCLUSIVE, + CivicSignificance.SENSITIVITY_RESPONSE.value: TherapeuticResponsePredicate.SENSITIVITY, + CivicSignificance.RESISTANCE: TherapeuticResponsePredicate.RESISTANCE, + CivicSignificance.POOR_OUTCOME: PrognosticPredicate.WORSE_OUTCOME, + CivicSignificance.BETTER_OUTCOME: PrognosticPredicate.BETTER_OUTCOME, + CivicSignificance.POSITIVE: DiagnosticPredicate.INCLUSIVE, + CivicSignificance.NEGATIVE: DiagnosticPredicate.EXCLUSIVE, + CivicSignificance.BENIGN: _IS_ONCOGENIC_FOR_PREDICATE, + CivicSignificance.LIKELY_BENIGN: _IS_ONCOGENIC_FOR_PREDICATE, + CivicSignificance.LIKELY_ONCOGENIC: _IS_ONCOGENIC_FOR_PREDICATE, + CivicSignificance.ONCOGENIC: _IS_ONCOGENIC_FOR_PREDICATE, + CivicSignificance.UNCERTAIN_SIGNIFICANCE: _IS_ONCOGENIC_FOR_PREDICATE, } ) @@ -681,7 +723,11 @@ def get_allele_origin_qualifier(record: Evidence | Assertion) -> MappableConcept def get_predicate( record: Evidence | Assertion, ) -> ( - PrognosticPredicate | DiagnosticPredicate | TherapeuticResponsePredicate | None + PrognosticPredicate + | DiagnosticPredicate + | TherapeuticResponsePredicate + | str + | None ): """Get GKS predicate @@ -757,10 +803,14 @@ def _get_proposition_params( "alleleOriginQualifier": self.get_allele_origin_qualifier(record), "predicate": self.get_predicate(record) if not is_clinical_significance_prop - else VariantClinicalSignificanceProposition.model_fields["predicate"].default, + else VariantClinicalSignificanceProposition.model_fields[ + "predicate" + ].default, } - if ( + if record_type == CivicEvidenceAssertionType.ONCOGENIC: + condition_key = "objectTumorType" + elif ( is_clinical_significance_prop or record_type != CivicEvidenceAssertionType.PREDICTIVE ): @@ -955,18 +1005,31 @@ def get_contributions(approval: Approval) -> list[Contribution]: def get_reported_in(assertion: Assertion) -> list[iriReference | Document]: """Get reported in information for an assertion + If multiple evidence items link to same source, will merge the source. + :param assertion: CIViC assertion record :return: List of CIViC links to records which the assertion is reported in """ reported_in: list[iriReference | Document] = [ iriReference(f"{LINKS_URL}/assertion/{assertion.id}") ] + civic_gks_sources = {} for evidence_item in assertion.evidence_items or []: - civic_gks_source = CivicGksSource( - evidence_item.source, - urls=[f"{LINKS_URL}/evidence/{evidence_item.id}"], - ) - reported_in.append(Document.model_validate(civic_gks_source)) + source = evidence_item.source + source_id = source.id + evidence_item_url = f"{LINKS_URL}/evidence/{evidence_item.id}" + + if source_id in civic_gks_sources: + civic_gks_sources[source_id].urls.append(evidence_item_url) + else: + civic_gks_sources[source_id] = Document.model_validate( + CivicGksSource( + source, + urls=[evidence_item_url], + ) + ) + reported_in.extend(list(civic_gks_sources.values())) + return reported_in @@ -1084,11 +1147,7 @@ def get_evidence_lines( :return: List of CIViC evidence lines :raise NotImplementedError: If evidence line type not supported """ - direction = ( - Direction.SUPPORTS - if assertion.assertion_direction == "SUPPORTS" - else Direction.DISPUTES - ) + direction = self.get_direction(assertion.assertion_direction) evidence_items: list[CivicGksEvidence] = [] for evidence_item in assertion.evidence_items: @@ -1142,11 +1201,123 @@ def get_proposition( return VariantClinicalSignificanceProposition(**params) +class CivicGksOncogenicAssertion( + VariantOncogenicityStatement, + _CivicGksAssertionMixin, + _CivicGksEvidenceAssertionMixin, +): + """Class for CIViC oncogenic assertion record represented as GKS""" + + def __init__(self, assertion: Assertion, approval: Approval | None = None) -> None: + """Initialize CivicGksOncogenicAssertion class + + :param assertion: CIViC assertion record + :param approval: CIViC approval for the assertion, defaults to None + :raises CivicGksRecordError: If CIViC assertion is not able to be represented as + GKS object + """ + if assertion.assertion_type not in ONCOGENIC_ASSERTION_TYPES: + err_msg = f"Assertion type must be one of {ONCOGENIC_ASSERTION_TYPES}" + raise CivicGksRecordError(err_msg) + + if not assertion.is_valid_for_gks_json(emit_warnings=True): + err_msg = "Assertion is not valid for GKS." + raise CivicGksRecordError(err_msg) + + contributions = self.get_contributions(approval) if approval else None + proposition = self.get_proposition(assertion) + classification, strength = self.get_classification_strength( + assertion.significance + ) + + super().__init__( + id=f"civic.aid:{assertion.id}", + contributions=contributions, + description=assertion.description, + specifiedBy=CCV_METHOD, + proposition=proposition, + direction=self.get_direction(assertion.assertion_direction), + classification=classification, + strength=strength, + hasEvidenceLines=self.get_evidence_lines(assertion), + reportedIn=self.get_reported_in(assertion), + ) + + def get_classification_strength( + self, significance + ) -> tuple[MappableConcept, MappableConcept | None]: + """Get classification and strength + + :param significance: Assertion's significance + :return: Classification and strength, if found + """ + _strength = None + + classification = MappableConcept( + primaryCoding=Coding( + code=code(CcvClassification[significance]), system=System.CCV + ) + ) + + if significance in { + CivicSignificance.LIKELY_BENIGN, + CivicSignificance.LIKELY_ONCOGENIC, + }: + _strength = StrengthCode.LIKELY + elif significance in {CivicSignificance.BENIGN, CivicSignificance.ONCOGENIC}: + _strength = StrengthCode.DEFINITIVE + + if _strength: + strength = MappableConcept( + primaryCoding=Coding(code=code(_strength.value), system=System.CCV) + ) + else: + strength = None + + return classification, strength + + def get_evidence_lines( + self, + assertion: Assertion, + ) -> list[VariantOncogenicityEvidenceLine]: + """Get evidence lines for a CIViC assertion + + :param assertion: CIViC assertion + :return: List of CIViC evidence lines + """ + direction = self.get_direction(assertion.assertion_direction) + + evidence_lines = [] + for clingen_code in assertion.clingen_codes or []: + evidence_attrs = derive_onco_evidence_attributes( + VariantOncogenicityEvidenceLine.Criterion(clingen_code.code) + ) + evidence_lines.append( + VariantOncogenicityEvidenceLine( + directionOfEvidenceProvided=direction, + **evidence_attrs.model_dump(), + ) + ) + + return evidence_lines + + def get_proposition(self, assertion: Assertion) -> VariantOncogenicityProposition: + """Get GKS proposition + + :param assertion: CIViC assertion record + :return: GKS proposition + """ + params = self._get_proposition_params( + assertion, assertion.assertion_type, is_clinical_significance_prop=False + ) + return VariantOncogenicityProposition(**params) + + def create_gks_record_from_assertion( assertion: Assertion, approval: Approval | None = None, submission_type_filter: ClinVarSubmissionType | None = None, -) -> CivicGksClinSigAssertion: +) -> CivicGksClinSigAssertion | CivicGksOncogenicAssertion: """Create GKS Record from CIViC Assertion :param assertion: CIViC assertion record @@ -1155,7 +1326,7 @@ def create_gks_record_from_assertion( restrict which assertion types may be translated :raises NotImplementedError: If GKS Record translation is not yet supported. Currently, only the following assertion types are supported: DIAGNOSTIC, - PREDICTIVE, and PROGNOSTIC. + PREDICTIVE, PROGNOSTIC, and ONCOGENIC. Or if the assertion type is excluded by the provided ClinVar submission type filter. :return: GKS Assertion Record object @@ -1173,5 +1344,8 @@ def create_gks_record_from_assertion( if assertion_type in CLINICAL_SIGNIFICANCE_ASSERTION_TYPES: return CivicGksClinSigAssertion(assertion, approval=approval) + if assertion_type in ONCOGENIC_ASSERTION_TYPES: + return CivicGksOncogenicAssertion(assertion, approval=approval) + err_msg = f"Assertion type {assertion_type} is not currently supported" raise NotImplementedError(err_msg) diff --git a/civicpy/exports/civic_gks_writer.py b/civicpy/exports/civic_gks_writer.py index 85be586..21c067d 100644 --- a/civicpy/exports/civic_gks_writer.py +++ b/civicpy/exports/civic_gks_writer.py @@ -8,7 +8,10 @@ from pydantic import BaseModel, Field -from civicpy.exports.civic_gks_record import CivicGksClinSigAssertion +from civicpy.exports.civic_gks_record import ( + CivicGksClinSigAssertion, + CivicGksOncogenicAssertion, +) def get_pkg_version(name: str) -> str: @@ -43,7 +46,7 @@ class GksAssertionError(BaseModel): class GksOutput(BaseModel): """Define model for representing GKS JSON output""" - gks_records: list[CivicGksClinSigAssertion] + gks_records: list[CivicGksClinSigAssertion | CivicGksOncogenicAssertion] metadata: GksOutputMetadata failed_assertion_ids: list[int] = [] errors: list[GksAssertionError] = [] @@ -60,7 +63,7 @@ class CivicGksWriter: def __init__( self, filepath: Path, - gks_records: list[CivicGksClinSigAssertion], + gks_records: list[CivicGksClinSigAssertion | CivicGksOncogenicAssertion], errors: list[GksAssertionError] | None = None, ): """Initialize CivicGksWriter class diff --git a/civicpy/tests/test_civic.py b/civicpy/tests/test_civic.py index 9194592..7fdaf36 100644 --- a/civicpy/tests/test_civic.py +++ b/civicpy/tests/test_civic.py @@ -1203,12 +1203,15 @@ def test_is_valid_for_gks_warnings_assertion(caplog): assert not not_accepted.is_valid_for_gks_json(emit_warnings=True) assert "Assertion 117 does not have 'accepted' status. Skipping" in caplog.text - oncogenic_fusion = civic.get_assertion_by_id(101) - assert not oncogenic_fusion.is_valid_for_gks_json(emit_warnings=True) + predisposing_assertion = civic.get_assertion_by_id(17) + assert not predisposing_assertion.is_valid_for_gks_json(emit_warnings=True) assert ( - "Assertion 101 type is not one of: 'DIAGNOSTIC', 'PREDICTIVE', or 'PROGNOSTIC'. Skipping" + "Assertion 17 type is not one of: 'DIAGNOSTIC', 'PREDICTIVE', 'PROGNOSTIC', or 'ONCOGENIC'. Skipping" in caplog.text ) + + oncogenic_fusion = civic.get_assertion_by_id(101) + assert not oncogenic_fusion.is_valid_for_gks_json(emit_warnings=True) assert "Assertion 101 variant is not a ``GeneVariant``. Skipping" in caplog.text complex_mp = civic.get_assertion_by_id(88) @@ -1220,10 +1223,6 @@ def test_is_valid_for_gks_warnings_evidence(caplog): """Test that is_valid_for_gks_json works correctly for evidence items""" not_accepted_oncogenic_fusion = civic.get_evidence_by_id(6936) assert not not_accepted_oncogenic_fusion.is_valid_for_gks_json(emit_warnings=True) - assert ( - "Evidence 6936 type is not one of: 'DIAGNOSTIC', 'PREDICTIVE', or 'PROGNOSTIC'. Skipping" - in caplog.text - ) assert "Evidence 6936 variant is not a ``GeneVariant``. Skipping" in caplog.text assert "Evidence 6936 does not have 'accepted' status. Skipping" in caplog.text diff --git a/civicpy/tests/test_exports.py b/civicpy/tests/test_exports.py index 6ea11ee..2a1e4da 100644 --- a/civicpy/tests/test_exports.py +++ b/civicpy/tests/test_exports.py @@ -2,19 +2,21 @@ from copy import deepcopy from unittest.mock import PropertyMock, patch -from ga4gh.core.models import iriReference import pytest from deepdiff import DeepDiff from ga4gh.va_spec.aac_2017 import ( VariantClinicalSignificanceStatement, ) from ga4gh.va_spec.base import Condition, ConditionSet, Statement, TherapyGroup +from ga4gh.va_spec.ccv_2022 import VariantOncogenicityStatement +from ga4gh.vrs.models import iriReference from civicpy import civic from civicpy.exports.civic_gks_record import ( CivicGksClinSigAssertion, CivicGksEvidence, CivicGksMolecularProfile, + CivicGksOncogenicAssertion, CivicGksRecordError, CivicGksTherapyGroup, ClinVarSubmissionType, @@ -672,6 +674,401 @@ def gks_aid115_object_condition(): } +def _ccv_method(method_type: str) -> dict: + """Get CCV Method""" + return { + "name": "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022", + "reportedIn": { + "id": "pmid:35101336", + "name": "Horak et al., 2022, Genet Med.", + "title": "Standards for the classification of pathogenicity of somatic variants in cancer (oncogenicity): Joint recommendations of Clinical Genome Resource (ClinGen), Cancer Genomics Consortium (CGC), and Variant Interpretation for Cancer Consortium (VICC)", + "doi": "10.1016/j.gim.2022.01.001", + "pmid": "35101336", + "urls": [ + "https://doi.org/10.1016/j.gim.2022.01.001", + "https://pubmed.ncbi.nlm.nih.gov/35101336/", + ], + "type": "Document", + }, + "methodType": method_type, + "type": "Method", + } + + +@pytest.fixture(scope="module") +def gks_gid42(): + """Create test fixture for CIViC GID42 GKS representation.""" + return { + "id": "civic.gid:42", + "conceptType": "Gene", + "name": "RET", + "mappings": [ + { + "coding": { + "id": "ncbigene:5979", + "code": "5979", + "system": "https://www.ncbi.nlm.nih.gov/gene/", + }, + "relation": "exactMatch", + }, + ], + "extensions": [ + { + "name": "description", + "value": "RET mutations and the RET fusion RET-PTC lead to activation of this tyrosine kinase receptor and are associated with thyroid cancers. RET point mutations are the most common mutations identified in medullary thyroid cancer (MTC) with germline and somatic mutations in RET associated with hereditary and sporadic forms, respectively. The most common somatic form mutation is M918T (exon 16) and a variety of other mutations effecting exons 10, 11 and 15 have been described. The prognostic significance of these mutations have been hotly debated in the field, however, data suggests that some RET mutation may confer drug resistance. Highly selective and well-tolerated RET inhibitors, selpercatinib (LOXO-292) and pralsetinib (BLU-667), have been FDA approved recently for the treatment of RET fusion-positive non-small-cell lung cancer, RET fusion-positive thyroid cancer and RET-mutant medullary thyroid cancer.", + }, + { + "name": "aliases", + "value": [ + "CDHF12", + "CDHR16", + "HSCR1", + "MEN2A", + "MEN2B", + "MTC1", + "PTC", + "RET", + "RET-ELE1", + ], + }, + ], + } + + +@pytest.fixture(scope="module") +def gks_aid202_proposition(gks_gid42): + """Create test fixture forCIVIC AID6 proposition""" + return { + "type": "VariantOncogenicityProposition", + "geneContextQualifier": gks_gid42, + "objectTumorType": { + "id": "civic.did:15", + "conceptType": "Disease", + "name": "Medullary Thyroid Carcinoma", + "mappings": [ + { + "coding": { + "code": "DOID:3973", + "system": "https://disease-ontology.org/?id=", + }, + "relation": "exactMatch", + } + ], + }, + "alleleOriginQualifier": { + "name": "somatic", + "mappings": [ + { + "coding": { + "code": "SOMATIC", + "system": "https://civicdb.org", + "iris": [ + "https://civic.readthedocs.io/en/latest/model/evidence/origin.html" + ], + }, + "relation": "exactMatch", + } + ], + }, + "predicate": "isOncogenicFor", + "subjectVariant": { + "id": "civic.mpid:113", + "type": "CategoricalVariant", + "description": "RET M918T is the most common somatically acquired mutation in medullary thyroid cancer (MTC). While there currently are no RET-specific inhibiting agents, promiscuous kinase inhibitors have seen some success in treating RET overactivity. Data suggests however, that the M918T mutation may lead to drug resistance, especially against the VEGFR-inhibitor motesanib. It has also been suggested that RET M918T leads to more aggressive MTC with a poorer prognosis.", + "name": "RET M918T", + "aliases": ["MET918THR"], + "mappings": [ + { + "coding": { + "code": "rs74799832", + "system": "https://www.ncbi.nlm.nih.gov/snp/", + }, + "relation": "relatedMatch", + }, + { + "coding": { + "code": "CA009082", + "system": "https://reg.clinicalgenome.org/redmine/projects/registry/genboree_registry/by_canonicalid?canonicalid=", + }, + "relation": "relatedMatch", + }, + { + "coding": { + "code": "13919", + "system": "https://www.ncbi.nlm.nih.gov/clinvar/variation/", + }, + "relation": "relatedMatch", + }, + { + "coding": { + "id": "civic.mpid:113", + "code": "113", + "system": "https://civicdb.org/links/molecular_profile/", + }, + "relation": "exactMatch", + }, + { + "coding": { + "code": "113", + "id": "civic.vid:113", + "name": "M918T", + "system": "https://civicdb.org/links/variant/", + "extensions": [ + {"name": "subtype", "value": "gene_variant"}, + { + "name": "variant_types", + "value": [ + { + "coding": { + "id": "civic.variant_type:47", + "code": "SO:0001583", + "name": "Missense Variant", + "system": "http://www.sequenceontology.org/browser/current_svn/term/", + }, + "relation": "exactMatch", + } + ], + }, + ], + }, + "relation": "exactMatch", + }, + ], + "extensions": [ + { + "name": "CIViC representative coordinate", + "value": { + "chromosome": "10", + "start": 43617416, + "stop": 43617416, + "reference_bases": "T", + "variant_bases": "C", + "representative_transcript": "ENST00000355710.3", + "ensembl_version": 75, + "reference_build": "GRCh37", + "type": "coordinates", + }, + }, + { + "name": "CIViC Molecular Profile Score", + "value": 139.0, + }, + { + "name": "expressions", + "value": [ + {"syntax": "hgvs.c", "value": "ENST00000355710.3:c.2753T>C"}, + {"syntax": "hgvs.c", "value": "NM_020975.4:c.2753T>C"}, + {"syntax": "hgvs.g", "value": "NC_000010.10:g.43617416T>C"}, + {"syntax": "hgvs.g", "value": "NC_000010.11:g.43121968T>C"}, + {"syntax": "hgvs.p", "value": "ENSP00000347942.3:p.Met918Thr"}, + {"syntax": "hgvs.p", "value": "NP_065681.1:p.Met918Thr"}, + { + "syntax": "hgvs.c", + "value": "ENST00000355710.8:c.2753T>C", + "extensions": [{"name": "is_mane_select", "value": True}], + }, + ], + }, + ], + }, + } + + +@pytest.fixture(scope="module") +def gks_aid202(gks_aid202_proposition): + """Create CIVIC AID6 GKS representation.""" + params = { + "id": "civic.aid:202", + "type": "Statement", + "description": "Published sequencing studies have shown that RET mutations are very common in medullary thryoid carcinoma (MTC) and M918T is the most common specific variant, especially in the MEN2B clinical subtype of familial disease (civic.EID:78) but also in sporadic cases(civic.EID:12800). M918T mutations may predict worse outcomes (civic.EID:74). Biochemical and functional characterization demonstrates that the M918T mutation leads to functional activation of RET relative to wild-type through multiple complementary mechanisms, including increased ATP affinity (>10-fold) and complex stability, reduced conformational rigidity, and the promotion of ligand-independent dimerization and autophosphorylation (civic.EID:12805). Exogenous expression has been shown to induce transformation of Ba/F3 cells (civic.EID:11723), and drive colony formation in NIH3T3 cells (civic.EID:12709, OS2). RET M918T occurs in the region of the tyrosine kinase domain which is associated with multiple endocrine neoplasia type 2 B (OM1). RET M918T is predicted to be deleterious (CHASMplus score 0.314 > VECS gene-specific cutoff of 0.22, OP1). Eleven instances of the variant occur in cancerhotspots.org (V2): 6 Thyroid, 4 Adrenal Gland, 1 Breast (OP3). The variant is absent in gnomAD database (v4.1.0, OP4). Together these criteria indicate that M918T is likely oncogenic, with a score of 9.", + "proposition": gks_aid202_proposition, + "strength": { + "primaryCoding": { + "code": "likely", + "system": "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022", + } + }, + "classification": { + "primaryCoding": { + "code": "likely oncogenic", + "system": "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022", + } + }, + "reportedIn": [ + "https://civicdb.org/links/assertion/202", + { + "id": "civic.sid:44", + "type": "Document", + "name": "Elisei et al., 2008", + "title": "Prognostic significance of somatic RET oncogene mutations in sporadic medullary thyroid cancer: a 10-year follow-up study.", + "urls": [ + "https://civicdb.org/links/evidence/74", + "https://civicdb.org/links/source/44", + "http://www.ncbi.nlm.nih.gov/pubmed/18073307", + "https://civicdb.org/links/evidence/12800", + ], + "pmid": "18073307", + }, + { + "id": "civic.sid:92", + "type": "Document", + "name": "Egawa et al., 1998", + "title": "Genotype-phenotype correlation of patients with multiple endocrine neoplasia type 2 in Japan.", + "urls": [ + "https://civicdb.org/links/evidence/78", + "https://civicdb.org/links/source/92", + "http://www.ncbi.nlm.nih.gov/pubmed/9839497", + ], + "pmid": "9839497", + }, + { + "id": "civic.sid:5458", + "type": "Document", + "name": "Romei et al., 2018", + "title": "RET mutation heterogeneity in primary advanced medullary thyroid cancers and their metastases.", + "urls": [ + "https://civicdb.org/links/evidence/12711", + "https://civicdb.org/links/source/5458", + "http://www.ncbi.nlm.nih.gov/pubmed/29515777", + "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5839408", + ], + "pmid": "29515777", + }, + { + "id": "civic.sid:5519", + "type": "Document", + "name": "Gujral et al., 2006", + "title": "Molecular mechanisms of RET receptor-mediated oncogenesis in multiple endocrine neoplasia 2B.", + "urls": [ + "https://civicdb.org/links/evidence/12805", + "https://civicdb.org/links/source/5519", + "http://www.ncbi.nlm.nih.gov/pubmed/17108110", + ], + "pmid": "17108110", + }, + { + "id": "civic.sid:4870", + "type": "Document", + "name": "Zhao et al., 2020", + "title": "Identifying novel oncogenic RET mutations and characterising their sensitivity to RET-specific inhibitors.", + "urls": [ + "https://civicdb.org/links/evidence/11723", + "https://civicdb.org/links/source/4870", + "http://www.ncbi.nlm.nih.gov/pubmed/32284345", + ], + "pmid": "32284345", + }, + { + "id": "civic.sid:4953", + "type": "Document", + "name": "Ceccherini et al., 1997", + "title": "Somatic in frame deletions not involving juxtamembranous cysteine residues strongly activate the RET proto-oncogene.", + "urls": [ + "https://civicdb.org/links/evidence/12709", + "https://civicdb.org/links/source/4953", + "http://www.ncbi.nlm.nih.gov/pubmed/9191060", + ], + "pmid": "9191060", + }, + ], + "direction": "supports", + "specifiedBy": _ccv_method("guideline"), + "hasEvidenceLines": [ + { + "type": "EvidenceLine", + "directionOfEvidenceProvided": "supports", + "strengthOfEvidenceProvided": { + "primaryCoding": { + "code": "moderate", + "system": "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022", + } + }, + "evidenceOutcome": { + "primaryCoding": { + "code": "OM1", + "system": "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022", + } + }, + "scoreOfEvidenceProvided": 2, + "specifiedBy": _ccv_method("functional_domain_location"), + }, + { + "type": "EvidenceLine", + "directionOfEvidenceProvided": "supports", + "strengthOfEvidenceProvided": { + "primaryCoding": { + "code": "strong", + "system": "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022", + } + }, + "evidenceOutcome": { + "primaryCoding": { + "code": "OS2", + "system": "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022", + } + }, + "scoreOfEvidenceProvided": 4, + "specifiedBy": _ccv_method("functional_assay"), + }, + { + "type": "EvidenceLine", + "directionOfEvidenceProvided": "supports", + "strengthOfEvidenceProvided": { + "primaryCoding": { + "code": "supporting", + "system": "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022", + } + }, + "evidenceOutcome": { + "primaryCoding": { + "code": "OP4", + "system": "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022", + } + }, + "scoreOfEvidenceProvided": 1, + "specifiedBy": _ccv_method("population_frequency"), + }, + { + "type": "EvidenceLine", + "directionOfEvidenceProvided": "supports", + "strengthOfEvidenceProvided": { + "primaryCoding": { + "code": "supporting", + "system": "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022", + } + }, + "evidenceOutcome": { + "primaryCoding": { + "code": "OP1", + "system": "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022", + } + }, + "scoreOfEvidenceProvided": 1, + "specifiedBy": _ccv_method("computational_prediction"), + }, + { + "type": "EvidenceLine", + "directionOfEvidenceProvided": "supports", + "strengthOfEvidenceProvided": { + "primaryCoding": { + "code": "supporting", + "system": "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022", + } + }, + "evidenceOutcome": { + "primaryCoding": { + "code": "OP3", + "system": "ClinGen/CGC/VICC Guidelines for Oncogenicity, 2022", + } + }, + "scoreOfEvidenceProvided": 1, + "specifiedBy": _ccv_method("somatic_hotspot_recurrence"), + }, + ], + } + return VariantOncogenicityStatement(**params) + + class TestCivicVcfRecord(object): def test_protein_altering(self, caplog, v600e): record = CivicVcfRecord(v600e) @@ -1027,6 +1424,75 @@ def test_invalid(self, aid117): CivicGksClinSigAssertion(aid117) +class TestCivicGksOncogenicAssertion(object): + """Test that CivicGksOncogenicAssertion works as expected""" + + def test_valid(self, aid202, gks_aid202): + """Test that valid oncogenic assertions works as expected""" + + def evidence_key(item: dict) -> str: + return item["evidenceOutcome"]["primaryCoding"]["code"] + + record = CivicGksOncogenicAssertion(aid202, approval=None) + assert isinstance(record, VariantOncogenicityStatement) + + actual = record.model_dump(exclude_none=True) + expected = gks_aid202.model_dump(exclude_none=True) + + assert set(actual.keys()) == set(expected.keys()) + + # Split out due to large record + for key in expected: + if key == "hasEvidenceLines": + actual_evidence = actual[key] + expected_evidence = expected[key] + + assert len(actual_evidence) == len(expected_evidence), ( + f"Mismatch in hasEvidenceLines length: " + f"actual={len(actual_evidence)}, expected={len(expected_evidence)}" + ) + + actual_by_code = {evidence_key(item): item for item in actual_evidence} + expected_by_code = { + evidence_key(item): item for item in expected_evidence + } + + assert set(actual_by_code) == set(expected_by_code), ( + "Mismatch in hasEvidence evidenceOutcome.primaryCoding.code values" + ) + + for code in expected_by_code: + diff = DeepDiff( + actual_by_code[code], + expected_by_code[code], + ignore_order=True, + ) + + assert diff == {}, ( + "Mismatch in hasEvidence item with " + f"evidenceOutcome.primaryCoding.code={code}" + ) + + continue + + diff = DeepDiff( + actual[key], + expected[key], + ignore_order=True, + ) + + assert diff == {}, f"Mismatch in key: {key}" + + def test_invalid(self, aid6): + """Test that unsupported assertion types raise exceptions""" + + with pytest.raises( + CivicGksRecordError, + match=re.escape("Assertion type must be one of ['ONCOGENIC']"), + ): + CivicGksOncogenicAssertion(aid6) + + class TestCivicGksRecord(object): """Test that GKS Record helper functions work correctly""" @@ -1046,6 +1512,11 @@ def test_unsupported_assertion_type(self): "should_raise_error", ), ( + [ + "aid202", + ClinVarSubmissionType.ONCOGENICITY, + False, + ], [ "aid202", ClinVarSubmissionType.CLINICAL_IMPACT, @@ -1056,16 +1527,31 @@ def test_unsupported_assertion_type(self): ClinVarSubmissionType.CLINICAL_IMPACT, False, ], + [ + "aid9", + ClinVarSubmissionType.ONCOGENICITY, + True, + ], [ "aid20", ClinVarSubmissionType.CLINICAL_IMPACT, False, ], + [ + "aid20", + ClinVarSubmissionType.ONCOGENICITY, + True, + ], [ "aid6", ClinVarSubmissionType.CLINICAL_IMPACT, False, ], + [ + "aid6", + ClinVarSubmissionType.ONCOGENICITY, + True, + ], ), ) def test_create_gks_record_from_assertion_filter( @@ -1097,3 +1583,11 @@ def test_clinvar_accession_ext(self): assert [ext.model_dump(exclude_none=True) for ext in record.extensions] == [ {"name": "clinvar_accession", "value": "SCV007542591"} ] + + def test_invalid_for_gks(self, aid117): + """Test that assertions invalid for GKS raise exceptions""" + + with pytest.raises( + CivicGksRecordError, match=r"Assertion is not valid for GKS." + ): + create_gks_record_from_assertion(aid117) diff --git a/docs/exports.rst b/docs/exports.rst index 0c1ceeb..35cdb8a 100644 --- a/docs/exports.rst +++ b/docs/exports.rst @@ -18,7 +18,7 @@ Knowledge Standards (GKS) objects. GKS JSON exports are maintained via the namespace:: >>>from civicpy.exports.civic_gks_writer import CivicGksWriter - >>>from civicpy.exports.civic_gks_record import CivicGksClinSigAssertion + >>>from civicpy.exports.civic_gks_record import CivicGksClinSigAssertion, CivicGksOncogenicAssertion Other file formats are planned for future releases. Suggestions are welcome on our `GitHub issues page `_. @@ -213,11 +213,12 @@ GKS JSON GKS JSON files are written using the :class:`civicpy.exports.civic_gks_writer.CivicGksWriter` class to which you add :class:`civicpy.exports.civic_gks_record.CivicGksClinSigAssertion` -during initialization. +or :class:`civicpy.exports.civic_gks_record.CivicGksOncogenicAssertion` during +initialization. In order to verify whether an assertion can be converted to a CivicGksClinSigAssertion -object, the convenience method ``is_valid_for_gks_json`` can be called on a -:class:`civic.Assertion` object. +or CivicGksOncogenicAssertion object, the convenience method ``is_valid_for_gks_json`` +can be called on a :class:`civic.Assertion` object. CivicGksClinSigAssertion ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -226,6 +227,13 @@ CivicGksClinSigAssertion :members: :show-inheritance: +CivicGksOncogenicAssertion +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autoclass:: civicpy.exports.civic_gks_record.CivicGksOncogenicAssertion + :members: + :show-inheritance: + CivicGksWriter ~~~~~~~~~~~~~~ @@ -237,38 +245,53 @@ Examples Here's an example of how to export all assertions to GKS JSON:: + from pathlib import Path + from civicpy import civic + from civicpy.exports.civic_gks_record import ( + CivicGksRecordError, + create_gks_record_from_assertion, + ) from civicpy.exports.civic_gks_writer import CivicGksWriter - from civicpy.exports.civic_gks_record import CivicGksClinSigAssertion records = [] for assertion in civic.get_all_assertions(): - if assertion.is_valid_for_gks_json(): - try: - gks_record = CivicGksClinSigAssertion(assertion) - except CivicGksRecordError: - continue + if assertion.is_valid_for_gks_json(): + try: + gks_record = create_gks_record_from_assertion(assertion) + except CivicGksRecordError: + continue + else: + records.append(gks_record) - records.append(gks_record) - CivicGksWriter("gks.json", records) + CivicGksWriter(Path("gks.json"), records) Here's an example of how to export all assertions approved by a specific organization that are ready for submission to ClinVar.:: + from pathlib import Path + from civicpy import civic + from civicpy.exports.civic_gks_record import ( + CivicGksRecordError, + create_gks_record_from_assertion, + ) from civicpy.exports.civic_gks_writer import CivicGksWriter - from civicpy.exports.civic_gks_record import CivicGksClinSigAssertion records = [] organization_id = 1 - for assertion in civic.get_all_assertions_ready_for_clinvar_submission_for_org(organization_id): - if assertion.is_valid_for_gks_json(): - try: - gks_record = CivicGksClinSigAssertion(assertion) - except CivicGksRecordError: - continue + for approval in civic.get_all_approvals_ready_for_clinvar_submission_for_org(organization_id): + assertion = approval.assertion + + if assertion.is_valid_for_gks_json(): + try: + gks_record = create_gks_record_from_assertion(assertion, approval=approval) + except CivicGksRecordError: + continue + else: + records.append(gks_record) + + CivicGksWriter(Path("gks.json"), records) - records.append(gks_record) - CivicGksWriter("gks.json", records) diff --git a/setup.py b/setup.py index 58282ac..7a7c2d2 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ "deprecation", "ga4gh.vrs~=2.4.0-a1", "ga4gh.cat_vrs~=0.8.0-a1", - "ga4gh.va_spec~=0.5.0-a0", + "ga4gh.va_spec~=0.5.0-a2", ], extras_require={ "test": [