diff --git a/civicpy/cli.py b/civicpy/cli.py index 0e19a99..dd7b125 100644 --- a/civicpy/cli.py +++ b/civicpy/cli.py @@ -5,7 +5,7 @@ from civicpy.__env__ import LOCAL_CACHE_PATH from civicpy.exports.civic_gks_record import ( CivicGksRecordError, - CivicGksAssertion + CivicGksClinSigAssertion ) from civicpy.exports.civic_gks_writer import CivicGksWriter, GksAssertionError from civicpy.exports.civic_vcf_writer import CivicVcfWriter @@ -98,7 +98,7 @@ def create_gks_json(organization_id: int, output_json: Path) -> None: logging.exception("Error getting organization %i", organization_id) return - records: list[CivicGksAssertion] = [] + records: list[CivicGksClinSigAssertion] = [] errors: list[GksAssertionError] = [] for approval in civic.get_all_approvals_ready_for_clinvar_submission_for_org( @@ -107,7 +107,7 @@ def create_gks_json(organization_id: int, output_json: Path) -> None: assertion = approval.assertion if assertion.is_valid_for_gks_json(emit_warnings=True): try: - gks_record = CivicGksAssertion(assertion, approval=approval) + gks_record = CivicGksClinSigAssertion(assertion, approval=approval) except (CivicGksRecordError, NotImplementedError) as e: errors.append( GksAssertionError(assertion_id=assertion.id, message=str(e)) diff --git a/civicpy/data/test_cache.pkl b/civicpy/data/test_cache.pkl index d699c5e..38de37c 100644 Binary files a/civicpy/data/test_cache.pkl and b/civicpy/data/test_cache.pkl differ diff --git a/civicpy/exports/civic_gks_record.py b/civicpy/exports/civic_gks_record.py index b9d0eba..3146e73 100644 --- a/civicpy/exports/civic_gks_record.py +++ b/civicpy/exports/civic_gks_record.py @@ -1,4 +1,8 @@ -"""Module for representing CIViC assertion record as GKS AAC 2017 Study Statement""" +"""Module for representing CIViC records as GKS representations + +* CIViC Predictive, Prognostic, and Diagnostic Assertions map to Variant + Clinical Significance Statements that follow the AMP/ASCO/CAP 2017 guidelines +""" from enum import Enum import logging @@ -623,6 +627,8 @@ def __init__( class _CivicGksEvidenceAssertionMixin: + """Mixin for CIViC Evidence and Assertions""" + @staticmethod def get_allele_origin_qualifier(record: Evidence | Assertion) -> MappableConcept: """Get GKS allele origin qualifier @@ -877,10 +883,53 @@ def __init__(self, evidence_item: Evidence) -> None: ) -class CivicGksAssertion( - VariantClinicalSignificanceStatement, _CivicGksEvidenceAssertionMixin +class _CivicGksAssertionMixin: + """Mixin for CIViC Assertions""" + + @staticmethod + def get_contributions(approval: Approval) -> list[Contribution]: + """Get contributions for an approval + + :param approval: Approval for assertion + :return: List of contributions, with one item containing when the approval was + last reviewed an organization. + Will include an extension, `is_approved_vcep`. + """ + organization: Organization = approval.organization + return [ + Contribution( + activityType=f"{approval.type}.last_reviewed", + date=approval.last_reviewed.split("T", 1)[0], + contributor=Agent( + id=f"civic.{organization.type}:{organization.id}", + name=organization.name, + description=organization.description, + extensions=[ + Extension( + name="is_approved_vcep", value=organization.is_approved_vcep + ) + ], + ), + ) + ] + + @staticmethod + def get_reported_in(assertion: Assertion) -> list[iriReference]: + """Get reported in information for an assertion + + :param assertion: CIViC assertion record + :return: List of CIViC links to records which the assertion is reported in + """ + return [iriReference(f"{LINKS_URL}/assertion/{assertion.id}")] + + +class CivicGksClinSigAssertion( + VariantClinicalSignificanceStatement, + _CivicGksAssertionMixin, + _CivicGksEvidenceAssertionMixin, ): - """Class for CIViC assertion record represented as GKS + """Class for CIViC predictive, prognostic, or diagnostic assertion record + represented as GKS :param assertion: CIViC assertion record :raises CivicGksRecordError: If CIViC assertion is not able to be represented as @@ -892,7 +941,7 @@ def __init__( assertion: Assertion, approval: Approval | None = None, ) -> None: - """Initialize _CivicGksAssertionRecord class + """Initialize CivicGksClinSigAssertion class :param assertion: CIViC assertion record :param approval: CIViC approval for the assertion, defaults to None @@ -924,35 +973,10 @@ def __init__( classification=classification, strength=strength, hasEvidenceLines=self.get_evidence_lines(assertion, level), - reportedIn=[iriReference(f"{LINKS_URL}/assertion/{assertion.id}")], + reportedIn=self.get_reported_in(assertion), extensions=extensions or None, ) - @staticmethod - def get_contributions(approval: Approval) -> list[Contribution]: - """Get contributions for an approval - - :param approval: Approval for assertion - :return: List of contributions containing when the approval was last reviewed - """ - organization: Organization = approval.organization - return [ - Contribution( - activityType=f"{approval.type}.last_reviewed", - date=approval.last_reviewed.split("T", 1)[0], - contributor=Agent( - id=f"civic.{organization.type}:{organization.id}", - name=organization.name, - description=organization.description, - extensions=[ - Extension( - name="is_approved_vcep", value=organization.is_approved_vcep - ) - ], - ), - ) - ] - def get_classification_strength_level( self, amp_level: str, diff --git a/civicpy/exports/civic_gks_writer.py b/civicpy/exports/civic_gks_writer.py index a96a2ef..85be586 100644 --- a/civicpy/exports/civic_gks_writer.py +++ b/civicpy/exports/civic_gks_writer.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, Field -from civicpy.exports.civic_gks_record import CivicGksAssertion +from civicpy.exports.civic_gks_record import CivicGksClinSigAssertion def get_pkg_version(name: str) -> str: @@ -43,7 +43,7 @@ class GksAssertionError(BaseModel): class GksOutput(BaseModel): """Define model for representing GKS JSON output""" - gks_records: list[CivicGksAssertion] + gks_records: list[CivicGksClinSigAssertion] metadata: GksOutputMetadata failed_assertion_ids: list[int] = [] errors: list[GksAssertionError] = [] @@ -60,7 +60,7 @@ class CivicGksWriter: def __init__( self, filepath: Path, - gks_records: list[CivicGksAssertion], + gks_records: list[CivicGksClinSigAssertion], errors: list[GksAssertionError] | None = None, ): """Initialize CivicGksWriter class diff --git a/civicpy/tests/test_exports.py b/civicpy/tests/test_exports.py index 2739d52..cf9c86a 100644 --- a/civicpy/tests/test_exports.py +++ b/civicpy/tests/test_exports.py @@ -14,7 +14,7 @@ CivicGksEvidence, CivicGksMolecularProfile, CivicGksRecordError, - CivicGksAssertion, + CivicGksClinSigAssertion, CivicGksTherapyGroup, ) @@ -751,12 +751,12 @@ def test_invalid(self, eid9285): CivicGksEvidence(eid9285) -class TestCivicGksAssertion(object): - """Test that CivicGksAssertion works as expected""" +class TestCivicGksClinSigAssertion(object): + """Test that CivicGksClinSigAssertion works as expected""" def test_valid_single_therapy(self, aid6, gks_aid6): """Test that single therapy works as expected""" - record = CivicGksAssertion(aid6) + record = CivicGksClinSigAssertion(aid6) assert isinstance(record, VariantClinicalSignificanceStatement) assert len(record.hasEvidenceLines) == 1 @@ -778,7 +778,7 @@ def test_valid_single_therapy(self, aid6, gks_aid6): def test_valid_combination_therapy(self, aid7): """Test that combination therapy works as expected""" - record = CivicGksAssertion(aid7) + record = CivicGksClinSigAssertion(aid7) assert isinstance(record, VariantClinicalSignificanceStatement) assert len(record.hasEvidenceLines) == 1 assert len(record.hasEvidenceLines[0].hasEvidenceItems) == 4 @@ -825,7 +825,7 @@ def test_valid_substitution_therapy( test_evidence_items.return_value = [] test_hgvs_expressions.return_value = None test_mane_select_transcript.return_value = None - record = CivicGksAssertion(aid19) + record = CivicGksClinSigAssertion(aid19) assert isinstance(record, VariantClinicalSignificanceStatement) assert len(record.hasEvidenceLines) == 1 therapy = record.hasEvidenceLines[0].targetProposition.objectTherapeutic.root @@ -837,7 +837,7 @@ def test_valid_substitution_therapy( def test_valid_prognostic(self, aid20): """Test that valid prognostic assertion works as expected""" - record = CivicGksAssertion(aid20) + record = CivicGksClinSigAssertion(aid20) assert isinstance(record, VariantClinicalSignificanceStatement) assert len(record.hasEvidenceLines) == 1 assert len(record.hasEvidenceLines[0].hasEvidenceItems) == 6 @@ -850,18 +850,20 @@ def test_valid_prognostic(self, aid20): @patch.object(civic.Assertion, "evidence_items", new_callable=PropertyMock) @patch.object(civic.Evidence, "is_valid_for_gks_json") - def test_citations(self, test_is_valid_for_gks_json,test_evidence_items, aid20): + def test_citations(self, test_is_valid_for_gks_json, test_evidence_items, aid20): """Test that citations extension is working correctly for EIDs that are not valid for GKS""" test_evidence_items.return_value = [civic.get_evidence_by_id(11881)] test_is_valid_for_gks_json.return_value = False - record = CivicGksAssertion(aid20) + record = CivicGksClinSigAssertion(aid20) assert len(record.hasEvidenceLines) == 1 assert record.hasEvidenceLines[0].hasEvidenceItems is None assert len(record.hasEvidenceLines[0].extensions) == 1 - assert record.hasEvidenceLines[0].extensions[0].model_dump(exclude_none=True) == { + assert record.hasEvidenceLines[0].extensions[0].model_dump( + exclude_none=True + ) == { "name": "citations", - "value": ["https://civicdb.org/links/evidence/11881"] + "value": ["https://civicdb.org/links/evidence/11881"], } @@ -877,7 +879,7 @@ def test_valid( gks_aid115_object_condition, ): """Test that valid diagnostic assertion works as expected""" - record = CivicGksAssertion(aid9) + record = CivicGksClinSigAssertion(aid9) assert isinstance(record, VariantClinicalSignificanceStatement) assert len(record.hasEvidenceLines) == 1 assert len(record.hasEvidenceLines[0].hasEvidenceItems) == 2 @@ -895,7 +897,7 @@ def test_valid( ) # Single phenotype (complex condition set) - record = CivicGksAssertion(aid93) + record = CivicGksClinSigAssertion(aid93) assert isinstance(record, VariantClinicalSignificanceStatement) record_object_condition = record.proposition.objectCondition assert isinstance(record_object_condition, Condition) @@ -908,7 +910,7 @@ def test_valid( assert diff == {} # Phenotypes (complex condition set) - record = CivicGksAssertion(aid115) + record = CivicGksClinSigAssertion(aid115) assert isinstance(record, VariantClinicalSignificanceStatement) record_object_condition = record.proposition.objectCondition assert isinstance(record_object_condition, Condition) @@ -922,7 +924,7 @@ def test_valid( def test_clinvar_accession_ext(self): a = civic.get_assertion_by_id(193) - record = CivicGksAssertion(a, approval=a.approvals[0]) + record = CivicGksClinSigAssertion(a, approval=a.approvals[0]) assert isinstance(record, VariantClinicalSignificanceStatement) assert [ext.model_dump(exclude_none=True) for ext in record.extensions] == [ {"name": "clinvar_accession", "value": "SCV007542591"} @@ -934,4 +936,4 @@ def test_invalid(self, aid117): with pytest.raises( CivicGksRecordError, match=r"Assertion is not valid for GKS." ): - CivicGksAssertion(aid117) + CivicGksClinSigAssertion(aid117) diff --git a/docs/exports.rst b/docs/exports.rst index 797a7ed..0c1ceeb 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 CivicGksAssertion + >>>from civicpy.exports.civic_gks_record import CivicGksClinSigAssertion Other file formats are planned for future releases. Suggestions are welcome on our `GitHub issues page `_. @@ -212,17 +212,17 @@ 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.CivicGksAssertion` +class to which you add :class:`civicpy.exports.civic_gks_record.CivicGksClinSigAssertion` during initialization. -In order to verify whether an assertion can be converted to a CivicGksAssertion +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. -CivicGksAssertion +CivicGksClinSigAssertion ~~~~~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: civicpy.exports.civic_gks_record.CivicGksAssertion +.. autoclass:: civicpy.exports.civic_gks_record.CivicGksClinSigAssertion :members: :show-inheritance: @@ -239,14 +239,14 @@ Here's an example of how to export all assertions to GKS JSON:: from civicpy import civic from civicpy.exports.civic_gks_writer import CivicGksWriter - from civicpy.exports.civic_gks_record import CivicGksAssertion + 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 = CivicGksAssertion(assertion) + gks_record = CivicGksClinSigAssertion(assertion) except CivicGksRecordError: continue @@ -258,7 +258,7 @@ ready for submission to ClinVar.:: from civicpy import civic from civicpy.exports.civic_gks_writer import CivicGksWriter - from civicpy.exports.civic_gks_record import CivicGksAssertion + from civicpy.exports.civic_gks_record import CivicGksClinSigAssertion records = [] organization_id = 1 @@ -266,7 +266,7 @@ ready for submission to ClinVar.:: 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 = CivicGksAssertion(assertion) + gks_record = CivicGksClinSigAssertion(assertion) except CivicGksRecordError: continue