From 127f0ab97f1ea30a4a773ea0a6764e8f20d877b9 Mon Sep 17 00:00:00 2001 From: Vadym Date: Mon, 29 Jul 2024 09:48:17 +0200 Subject: [PATCH] Add delivery report Raredisease (#3459) ### Added: - Delivery report Raredisease --- cg/cli/generate/report/utils.py | 5 + cg/cli/workflow/balsamic/options.py | 4 +- cg/clients/chanjo2/models.py | 6 + cg/constants/constants.py | 6 +- cg/constants/delivery.py | 8 +- cg/constants/nf_analysis.py | 4 + cg/constants/report.py | 32 +++-- cg/constants/scout.py | 21 +++- cg/meta/report/balsamic.py | 46 +++---- cg/meta/report/balsamic_umi.py | 5 - cg/meta/report/mip_dna.py | 63 +++++----- cg/meta/report/raredisease.py | 117 ++++++++++++++++++ cg/meta/report/report_api.py | 42 ++----- cg/meta/report/rnafusion.py | 43 ++++--- cg/meta/report/taxprofiler.py | 32 ++--- .../macros/data_analysis/data_analysis.html | 8 +- .../qc_metrics/mip_dna_qc_metrics.html | 2 +- .../qc_metrics/raredisease_qc_metrics.html | 16 +++ cg/meta/report/templates/macros/order.html | 6 +- .../report/templates/macros/sample_prep.html | 4 +- .../raredisease_uploaded_files.html | 37 ++++++ .../macros/uploaded_files/uploaded_files.html | 4 + cg/meta/report/tomte.py | 31 ++--- cg/meta/workflow/analysis.py | 33 ++++- cg/meta/workflow/balsamic.py | 5 + cg/meta/workflow/balsamic_umi.py | 5 + cg/meta/workflow/mip_dna.py | 7 +- cg/meta/workflow/raredisease.py | 57 ++++++++- cg/meta/workflow/rnafusion.py | 8 +- cg/models/analysis.py | 5 +- cg/models/raredisease/raredisease.py | 19 ++- cg/models/report/metadata.py | 22 +++- cg/models/report/report.py | 14 +++ cg/models/report/sample.py | 10 +- cg/models/tomte/tomte.py | 8 +- tests/clients/chanjo2/test_chanjo2_client.py | 34 ++++- tests/conftest.py | 2 +- tests/meta/report/test_mip_dna_api.py | 2 +- tests/meta/report/test_report_api.py | 2 +- tests/mocks/report.py | 9 +- 40 files changed, 581 insertions(+), 203 deletions(-) create mode 100644 cg/meta/report/raredisease.py create mode 100644 cg/meta/report/templates/macros/data_analysis/qc_metrics/raredisease_qc_metrics.html create mode 100644 cg/meta/report/templates/macros/uploaded_files/raredisease_uploaded_files.html diff --git a/cg/cli/generate/report/utils.py b/cg/cli/generate/report/utils.py index 09eb53d427..920827a2af 100644 --- a/cg/cli/generate/report/utils.py +++ b/cg/cli/generate/report/utils.py @@ -14,6 +14,7 @@ from cg.meta.report.balsamic_qc import BalsamicQCReportAPI from cg.meta.report.balsamic_umi import BalsamicUmiReportAPI from cg.meta.report.mip_dna import MipDNAReportAPI +from cg.meta.report.raredisease import RarediseaseReportAPI from cg.meta.report.report_api import ReportAPI from cg.meta.report.rnafusion import RnafusionReportAPI from cg.meta.report.taxprofiler import TaxprofilerReportAPI @@ -22,6 +23,7 @@ from cg.meta.workflow.balsamic_qc import BalsamicQCAnalysisAPI from cg.meta.workflow.balsamic_umi import BalsamicUmiAnalysisAPI from cg.meta.workflow.mip_dna import MipDNAAnalysisAPI +from cg.meta.workflow.raredisease import RarediseaseAnalysisAPI from cg.meta.workflow.rnafusion import RnafusionAnalysisAPI from cg.meta.workflow.taxprofiler import TaxprofilerAnalysisAPI from cg.meta.workflow.tomte import TomteAnalysisAPI @@ -98,6 +100,9 @@ def get_report_api_workflow(context: click.Context, workflow: Workflow) -> Repor Workflow.MIP_DNA: MipDNAReportAPI( config=context.obj, analysis_api=MipDNAAnalysisAPI(config=context.obj) ), + Workflow.RAREDISEASE: RarediseaseReportAPI( + config=context.obj, analysis_api=RarediseaseAnalysisAPI(config=context.obj) + ), Workflow.RNAFUSION: RnafusionReportAPI( config=context.obj, analysis_api=RnafusionAnalysisAPI(config=context.obj) ), diff --git a/cg/cli/workflow/balsamic/options.py b/cg/cli/workflow/balsamic/options.py index f4c215e43d..4c47c3a0f8 100644 --- a/cg/cli/workflow/balsamic/options.py +++ b/cg/cli/workflow/balsamic/options.py @@ -13,8 +13,8 @@ OPTION_GENOME_VERSION = click.option( "--genome-version", show_default=True, - default=GenomeVersion.hg19, - type=click.Choice([GenomeVersion.hg19, GenomeVersion.hg38, GenomeVersion.canfam3]), + default=GenomeVersion.HG19, + type=click.Choice([GenomeVersion.HG19, GenomeVersion.HG38, GenomeVersion.CANFAM3]), help="Type and build version of the reference genome. Set this option to override the default.", ) OPTION_PANEL_BED = click.option( diff --git a/cg/clients/chanjo2/models.py b/cg/clients/chanjo2/models.py index f24c49e7e1..3a1167ac10 100644 --- a/cg/clients/chanjo2/models.py +++ b/cg/clients/chanjo2/models.py @@ -38,3 +38,9 @@ def root_must_not_be_empty(cls, root: dict[str, CoverageMetrics]): if not root: raise ValueError("Coverage POST response must not be an empty dictionary") return root + + def get_sample_coverage_metrics(self, sample_id: str) -> CoverageMetrics: + """Return the coverage metrics for the specified sample ID.""" + if sample_id not in self.root: + raise ValueError(f"Sample ID '{sample_id}' not found in the coverage POST response") + return self.root[sample_id] diff --git a/cg/constants/constants.py b/cg/constants/constants.py index c1d8d7d6a6..75d506bf3e 100644 --- a/cg/constants/constants.py +++ b/cg/constants/constants.py @@ -165,9 +165,9 @@ class GenomeVersion(StrEnum): GRCh37: str = "GRCh37" GRCh38: str = "GRCh38" T2T_CHM13: str = "T2T-CHM13v2.0" - canfam3: str = "canfam3" - hg19: str = "hg19" - hg38: str = "hg38" + CANFAM3 = auto() + HG19 = auto() + HG38 = auto() class SampleType(StrEnum): diff --git a/cg/constants/delivery.py b/cg/constants/delivery.py index 5368a11fbe..a57ab10a6e 100644 --- a/cg/constants/delivery.py +++ b/cg/constants/delivery.py @@ -211,13 +211,17 @@ "case_tags": RNAFUSION_ANALYSIS_CASE_TAGS, "sample_tags": RNAFUSION_ANALYSIS_SAMPLE_TAGS, }, + Workflow.RAREDISEASE: { + "case_tags": NF_ANALYSIS_CASE_TAGS, + "sample_tags": NF_ANALYSIS_SAMPLE_TAGS, + }, Workflow.TAXPROFILER: { "case_tags": NF_ANALYSIS_CASE_TAGS, - "sample_tags": NF_ANALYSIS_CASE_TAGS, + "sample_tags": NF_ANALYSIS_SAMPLE_TAGS, }, Workflow.TOMTE: { "case_tags": NF_ANALYSIS_CASE_TAGS, - "sample_tags": NF_ANALYSIS_CASE_TAGS, + "sample_tags": NF_ANALYSIS_SAMPLE_TAGS, }, } diff --git a/cg/constants/nf_analysis.py b/cg/constants/nf_analysis.py index 78c0a40569..031327f500 100644 --- a/cg/constants/nf_analysis.py +++ b/cg/constants/nf_analysis.py @@ -48,3 +48,7 @@ class NfTowerStatus(StrEnum): } } """ + +RAREDISEASE_COVERAGE_FILE_TAGS: list[str] = ["coverage", "d4"] +RAREDISEASE_COVERAGE_INTERVAL_TYPE: str = "genes" +RAREDISEASE_COVERAGE_THRESHOLD: int = 10 diff --git a/cg/constants/report.py b/cg/constants/report.py index 36ebb2973d..86a8890bdb 100644 --- a/cg/constants/report.py +++ b/cg/constants/report.py @@ -22,6 +22,7 @@ Workflow.BALSAMIC_QC, Workflow.BALSAMIC_UMI, Workflow.MIP_DNA, + Workflow.RAREDISEASE, Workflow.RNAFUSION, Workflow.TAXPROFILER, Workflow.TOMTE, @@ -102,10 +103,12 @@ "genome_build", ] -REQUIRED_DATA_ANALYSIS_MIP_DNA_FIELDS: list[str] = REQUIRED_DATA_ANALYSIS_FIELDS + [ +REQUIRED_DATA_ANALYSIS_RAREDISEASE_FIELDS: list[str] = REQUIRED_DATA_ANALYSIS_FIELDS + [ "panels", ] +REQUIRED_DATA_ANALYSIS_MIP_DNA_FIELDS: list[str] = REQUIRED_DATA_ANALYSIS_RAREDISEASE_FIELDS + REQUIRED_DATA_ANALYSIS_BALSAMIC_FIELDS: list[str] = REQUIRED_DATA_ANALYSIS_FIELDS + [ "type", "variant_callers", @@ -122,7 +125,7 @@ "name", "id", "ticket", - "gender", + "sex", "source", "application", "methods", @@ -130,7 +133,7 @@ "timestamps", ] -_REQUIRED_SAMPLE_RARE_DISEASE_FIELDS: list[str] = _REQUIRED_SAMPLE_FIELDS + [ +REQUIRED_SAMPLE_RAREDISEASE_FIELDS: list[str] = _REQUIRED_SAMPLE_FIELDS + [ "status", ] @@ -138,7 +141,7 @@ "tumour", ] -REQUIRED_SAMPLE_MIP_DNA_FIELDS: list[str] = _REQUIRED_SAMPLE_RARE_DISEASE_FIELDS +REQUIRED_SAMPLE_MIP_DNA_FIELDS: list[str] = REQUIRED_SAMPLE_RAREDISEASE_FIELDS REQUIRED_SAMPLE_BALSAMIC_FIELDS: list[str] = _REQUIRED_SAMPLE_CANCER_FIELDS @@ -146,7 +149,7 @@ REQUIRED_SAMPLE_TAXPROFILER_FIELDS: list[str] = _REQUIRED_SAMPLE_FIELDS -REQUIRED_SAMPLE_TOMTE_FIELDS: list[str] = _REQUIRED_SAMPLE_RARE_DISEASE_FIELDS +REQUIRED_SAMPLE_TOMTE_FIELDS: list[str] = REQUIRED_SAMPLE_RAREDISEASE_FIELDS # Methods required fields (OPTIONAL: "library_prep", "sequencing") REQUIRED_SAMPLE_METHODS_FIELDS: list[str] = [] @@ -164,16 +167,25 @@ "million_read_pairs", ] -REQUIRED_SAMPLE_METADATA_MIP_DNA_WGS_FIELDS: list[str] = _REQUIRED_SAMPLE_METADATA_FIELDS + [ - "gender", +REQUIRED_SAMPLE_METADATA_RAREDISEASE_WGS_FIELDS: list[str] = _REQUIRED_SAMPLE_METADATA_FIELDS + [ + "sex", "mapped_reads", "mean_target_coverage", "pct_10x", ] -REQUIRED_SAMPLE_METADATA_MIP_DNA_FIELDS: list[str] = REQUIRED_SAMPLE_METADATA_MIP_DNA_WGS_FIELDS + [ - "bait_set", -] +REQUIRED_SAMPLE_METADATA_RAREDISEASE_FIELDS: list[str] = ( + REQUIRED_SAMPLE_METADATA_RAREDISEASE_WGS_FIELDS + + [ + "bait_set", + ] +) + +REQUIRED_SAMPLE_METADATA_MIP_DNA_WGS_FIELDS: list[str] = ( + REQUIRED_SAMPLE_METADATA_RAREDISEASE_WGS_FIELDS +) + +REQUIRED_SAMPLE_METADATA_MIP_DNA_FIELDS: list[str] = REQUIRED_SAMPLE_METADATA_RAREDISEASE_FIELDS _REQUIRED_SAMPLE_METADATA_BALSAMIC_FIELDS: list[str] = _REQUIRED_SAMPLE_METADATA_FIELDS + [ "mean_insert_size", diff --git a/cg/constants/scout.py b/cg/constants/scout.py index a59f898078..83f87cf30f 100644 --- a/cg/constants/scout.py +++ b/cg/constants/scout.py @@ -1,9 +1,12 @@ -from enum import StrEnum +from enum import StrEnum, auto from cg.constants import FileExtensions from cg.constants.housekeeper_tags import AlignmentFileTag +HGNC_ID = "hgnc_id" + + class GenomeBuild(StrEnum): hg19: str = "37" hg38: str = "38" @@ -29,7 +32,15 @@ class ScoutCustomCaseReportTags(StrEnum): GENE_FUSION_RESEARCH: str = "gene_fusion_research" -MIP_CASE_TAGS = dict( +class ScoutUploadKey(StrEnum): + SMN_TSV = auto() + SNV_VCF = auto() + SV_VCF = auto() + VCF_STR = auto() + VCF_FUSION = auto() + + +RAREDISEASE_CASE_TAGS = dict( delivery_report={"delivery-report"}, multiqc_report={"multiqc-html"}, peddy_check={"ped-check", "peddy"}, @@ -46,6 +57,8 @@ class ScoutCustomCaseReportTags(StrEnum): vcf_str={"vcf-str"}, ) +MIP_CASE_TAGS: dict[str, set[str]] = RAREDISEASE_CASE_TAGS + BALSAMIC_CASE_TAGS = dict( sv_vcf={"vcf-sv-clinical"}, snv_vcf={"vcf-snv-clinical"}, @@ -74,7 +87,7 @@ class ScoutCustomCaseReportTags(StrEnum): vcf_fusion={"vcf-fusion"}, ) -MIP_SAMPLE_TAGS = dict( +RAREDISEASE_SAMPLE_TAGS = dict( bam_file={"bam"}, alignment_file={"cram"}, vcf2cytosure={"vcf2cytosure"}, @@ -89,6 +102,8 @@ class ScoutCustomCaseReportTags(StrEnum): mitodel_file={"mitodel"}, ) +MIP_SAMPLE_TAGS: dict[str, set[str]] = RAREDISEASE_SAMPLE_TAGS + BALSAMIC_SAMPLE_TAGS = dict( bam_file={"bam"}, alignment_file={"cram"}, diff --git a/cg/meta/report/balsamic.py b/cg/meta/report/balsamic.py index dc87c6f790..7950ea98de 100644 --- a/cg/meta/report/balsamic.py +++ b/cg/meta/report/balsamic.py @@ -18,7 +18,7 @@ Workflow, ) from cg.constants.constants import AnalysisType -from cg.constants.scout import BALSAMIC_CASE_TAGS +from cg.constants.scout import ScoutUploadKey from cg.meta.report.field_validators import get_million_read_pairs from cg.meta.report.report_api import ReportAPI from cg.meta.workflow.balsamic import BalsamicAnalysisAPI @@ -33,7 +33,7 @@ BalsamicTargetedSampleMetadataModel, BalsamicWGSSampleMetadataModel, ) -from cg.models.report.report import CaseModel, ScoutReportFiles +from cg.models.report.report import CaseModel, ScoutReportFiles, ReportRequiredFields from cg.models.report.sample import SampleModel from cg.store.models import Bed, BedVersion, Case, Sample @@ -139,8 +139,12 @@ def is_report_accredited( def get_scout_uploaded_files(self, case_id: str) -> ScoutReportFiles: """Return files that will be uploaded to Scout.""" return ScoutReportFiles( - snv_vcf=self.get_scout_uploaded_file_from_hk(case_id=case_id, scout_tag="snv_vcf"), - sv_vcf=self.get_scout_uploaded_file_from_hk(case_id=case_id, scout_tag="sv_vcf"), + snv_vcf=self.get_scout_uploaded_file_from_hk( + case_id=case_id, scout_key=ScoutUploadKey.SNV_VCF + ), + sv_vcf=self.get_scout_uploaded_file_from_hk( + case_id=case_id, scout_key=ScoutUploadKey.SV_VCF + ), ) def get_required_fields(self, case: CaseModel) -> dict: @@ -167,28 +171,26 @@ def get_required_fields(self, case: CaseModel) -> dict: required_sample_metadata_fields: list[str] = ( REQUIRED_SAMPLE_METADATA_BALSAMIC_TARGETED_FIELDS ) - return { - "report": REQUIRED_REPORT_FIELDS, - "customer": REQUIRED_CUSTOMER_FIELDS, - "case": REQUIRED_CASE_FIELDS, - "applications": self.get_application_required_fields( + + report_required_fields = ReportRequiredFields( + applications=self.get_application_required_fields( case=case, required_fields=REQUIRED_APPLICATION_FIELDS ), - "data_analysis": required_data_analysis_fields, - "samples": self.get_sample_required_fields( - case=case, required_fields=REQUIRED_SAMPLE_BALSAMIC_FIELDS + case=REQUIRED_CASE_FIELDS, + customer=REQUIRED_CUSTOMER_FIELDS, + data_analysis=required_data_analysis_fields, + metadata=self.get_sample_required_fields( + case=case, required_fields=required_sample_metadata_fields ), - "methods": self.get_sample_required_fields( + methods=self.get_sample_required_fields( case=case, required_fields=REQUIRED_SAMPLE_METHODS_FIELDS ), - "timestamps": self.get_timestamp_required_fields( - case=case, required_fields=REQUIRED_SAMPLE_TIMESTAMP_FIELDS + report=REQUIRED_REPORT_FIELDS, + samples=self.get_sample_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_BALSAMIC_FIELDS ), - "metadata": self.get_sample_required_fields( - case=case, required_fields=required_sample_metadata_fields + timestamps=self.get_timestamp_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_TIMESTAMP_FIELDS ), - } - - def get_upload_case_tags(self) -> dict: - """Return Balsamic upload case tags.""" - return BALSAMIC_CASE_TAGS + ) + return report_required_fields.model_dump() diff --git a/cg/meta/report/balsamic_umi.py b/cg/meta/report/balsamic_umi.py index 233c92622c..4f36b5ff11 100644 --- a/cg/meta/report/balsamic_umi.py +++ b/cg/meta/report/balsamic_umi.py @@ -1,6 +1,5 @@ import logging -from cg.constants.scout import BALSAMIC_UMI_CASE_TAGS from cg.meta.report.balsamic import BalsamicReportAPI from cg.meta.workflow.balsamic_umi import BalsamicUmiAnalysisAPI from cg.models.cg_config import CGConfig @@ -14,7 +13,3 @@ class BalsamicUmiReportAPI(BalsamicReportAPI): def __init__(self, config: CGConfig, analysis_api: BalsamicUmiAnalysisAPI): super().__init__(config=config, analysis_api=analysis_api) self.analysis_api: BalsamicUmiAnalysisAPI = analysis_api - - def get_upload_case_tags(self) -> dict: - """Return Balsamic UMI upload case tags.""" - return BALSAMIC_UMI_CASE_TAGS diff --git a/cg/meta/report/mip_dna.py b/cg/meta/report/mip_dna.py index 6d2a79080f..eb05ae33b0 100644 --- a/cg/meta/report/mip_dna.py +++ b/cg/meta/report/mip_dna.py @@ -15,7 +15,7 @@ REQUIRED_SAMPLE_MIP_DNA_FIELDS, REQUIRED_SAMPLE_TIMESTAMP_FIELDS, ) -from cg.constants.scout import MIP_CASE_TAGS +from cg.constants.scout import ScoutUploadKey from cg.meta.report.field_validators import get_million_read_pairs from cg.meta.report.report_api import ReportAPI from cg.meta.workflow.mip_dna import MipDNAAnalysisAPI @@ -23,7 +23,7 @@ from cg.models.mip.mip_analysis import MipAnalysis from cg.models.mip.mip_metrics_deliverables import get_sample_id_metric from cg.models.report.metadata import MipDNASampleMetadataModel -from cg.models.report.report import CaseModel, ScoutReportFiles +from cg.models.report.report import CaseModel, ScoutReportFiles, ReportRequiredFields from cg.models.report.sample import SampleModel from cg.store.models import Case, Sample @@ -47,7 +47,7 @@ def get_sample_metadata( return MipDNASampleMetadataModel( bait_set=self.lims_api.capture_kit(sample.internal_id), duplicates=parsed_metrics.duplicate_reads, - gender=parsed_metrics.predicted_sex, + sex=parsed_metrics.predicted_sex, initial_qc=self.lims_api.has_sample_passed_initial_qc(sample.internal_id), mapped_reads=parsed_metrics.mapped_reads, mean_target_coverage=sample_coverage.get("mean_coverage"), @@ -89,33 +89,42 @@ def is_report_accredited( def get_scout_uploaded_files(self, case_id: str) -> ScoutReportFiles: """Return files that will be uploaded to Scout.""" return ScoutReportFiles( - snv_vcf=self.get_scout_uploaded_file_from_hk(case_id=case_id, scout_tag="snv_vcf"), - sv_vcf=self.get_scout_uploaded_file_from_hk(case_id=case_id, scout_tag="sv_vcf"), - vcf_str=self.get_scout_uploaded_file_from_hk(case_id=case_id, scout_tag="vcf_str"), - smn_tsv=self.get_scout_uploaded_file_from_hk(case_id=case_id, scout_tag="smn_tsv"), + snv_vcf=self.get_scout_uploaded_file_from_hk( + case_id=case_id, scout_key=ScoutUploadKey.SNV_VCF + ), + sv_vcf=self.get_scout_uploaded_file_from_hk( + case_id=case_id, scout_key=ScoutUploadKey.SV_VCF + ), + vcf_str=self.get_scout_uploaded_file_from_hk( + case_id=case_id, scout_key=ScoutUploadKey.VCF_STR + ), + smn_tsv=self.get_scout_uploaded_file_from_hk( + case_id=case_id, scout_key=ScoutUploadKey.SMN_TSV + ), ) def get_required_fields(self, case: CaseModel) -> dict: """Return dictionary with the delivery report required fields for MIP DNA.""" - return { - "report": REQUIRED_REPORT_FIELDS, - "customer": REQUIRED_CUSTOMER_FIELDS, - "case": REQUIRED_CASE_FIELDS, - "applications": self.get_application_required_fields( + report_required_fields = ReportRequiredFields( + applications=self.get_application_required_fields( case=case, required_fields=REQUIRED_APPLICATION_FIELDS ), - "data_analysis": REQUIRED_DATA_ANALYSIS_MIP_DNA_FIELDS, - "samples": self.get_sample_required_fields( - case=case, required_fields=REQUIRED_SAMPLE_MIP_DNA_FIELDS - ), - "methods": self.get_sample_required_fields( + case=REQUIRED_CASE_FIELDS, + customer=REQUIRED_CUSTOMER_FIELDS, + data_analysis=REQUIRED_DATA_ANALYSIS_MIP_DNA_FIELDS, + metadata=self.get_sample_metadata_required_fields(case=case), + methods=self.get_sample_required_fields( case=case, required_fields=REQUIRED_SAMPLE_METHODS_FIELDS ), - "timestamps": self.get_timestamp_required_fields( + report=REQUIRED_REPORT_FIELDS, + samples=self.get_sample_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_MIP_DNA_FIELDS + ), + timestamps=self.get_timestamp_required_fields( case=case, required_fields=REQUIRED_SAMPLE_TIMESTAMP_FIELDS ), - "metadata": self.get_sample_metadata_required_fields(case=case), - } + ) + return report_required_fields.model_dump() @staticmethod def get_sample_metadata_required_fields(case: CaseModel) -> dict: @@ -130,20 +139,16 @@ def get_sample_metadata_required_fields(case: CaseModel) -> dict: required_sample_metadata_fields.update({sample.id: required_fields}) return required_sample_metadata_fields - def get_upload_case_tags(self) -> dict: - """Return MIP DNA upload case tags.""" - return MIP_CASE_TAGS - - def get_scout_uploaded_file_from_hk(self, case_id: str, scout_tag: str) -> str | None: + def get_scout_uploaded_file_from_hk( + self, case_id: str, scout_key: ScoutUploadKey + ) -> str | None: """Return file path of the uploaded to Scout file given its tag.""" version: Version = self.housekeeper_api.last_version(bundle=case_id) - tags: list = self.get_hk_scout_file_tags(scout_tag=scout_tag) + tags: list = self.get_hk_scout_file_tags(scout_key=scout_key) uploaded_files: Iterable[File] = self.housekeeper_api.get_files( bundle=case_id, tags=tags, version=version.id ) if not tags or not any(uploaded_files): - LOG.info( - f"No files were found for the following Scout Housekeeper tag: {scout_tag} (case: {case_id})" - ) + LOG.info(f"No files were found for the following Scout key: {scout_key}") return None return uploaded_files[0].full_path diff --git a/cg/meta/report/raredisease.py b/cg/meta/report/raredisease.py new file mode 100644 index 0000000000..ebb4dc5a62 --- /dev/null +++ b/cg/meta/report/raredisease.py @@ -0,0 +1,117 @@ +"""Raredisease Delivery Report API.""" + +from cg.clients.chanjo2.models import CoverageMetrics +from cg.constants.constants import PrepCategory +from cg.constants.report import ( + REQUIRED_APPLICATION_FIELDS, + REQUIRED_CASE_FIELDS, + REQUIRED_CUSTOMER_FIELDS, + REQUIRED_DATA_ANALYSIS_RAREDISEASE_FIELDS, + REQUIRED_REPORT_FIELDS, + REQUIRED_SAMPLE_METADATA_RAREDISEASE_FIELDS, + REQUIRED_SAMPLE_METADATA_RAREDISEASE_WGS_FIELDS, + REQUIRED_SAMPLE_METHODS_FIELDS, + REQUIRED_SAMPLE_RAREDISEASE_FIELDS, + REQUIRED_SAMPLE_TIMESTAMP_FIELDS, +) +from cg.constants.scout import ScoutUploadKey +from cg.meta.report.field_validators import get_million_read_pairs +from cg.meta.report.report_api import ReportAPI +from cg.meta.workflow.raredisease import RarediseaseAnalysisAPI +from cg.models.analysis import AnalysisModel, NextflowAnalysis +from cg.models.cg_config import CGConfig +from cg.models.raredisease.raredisease import RarediseaseQCMetrics +from cg.models.report.metadata import RarediseaseSampleMetadataModel +from cg.models.report.report import CaseModel, ScoutReportFiles, ReportRequiredFields +from cg.models.report.sample import SampleModel +from cg.store.models import Case, Sample + + +class RarediseaseReportAPI(ReportAPI): + """API to create Raredisease delivery reports.""" + + def __init__(self, config: CGConfig, analysis_api: RarediseaseAnalysisAPI): + super().__init__(config=config, analysis_api=analysis_api) + + def get_sample_metadata( + self, case: Case, sample: Sample, analysis_metadata: NextflowAnalysis + ) -> RarediseaseSampleMetadataModel: + """Return Raredisease sample metadata to include in the report.""" + sample_metrics: RarediseaseQCMetrics = analysis_metadata.sample_metrics[sample.internal_id] + gene_ids: list[int] = self.analysis_api.get_gene_ids_from_scout(case.panels) + coverage_metrics: CoverageMetrics | None = self.analysis_api.get_sample_coverage( + case_id=case.internal_id, sample_id=sample.internal_id, gene_ids=gene_ids + ) + return RarediseaseSampleMetadataModel( + bait_set=self.lims_api.capture_kit(sample.internal_id), + duplicates=sample_metrics.percent_duplicates, + initial_qc=self.lims_api.has_sample_passed_initial_qc(sample.internal_id), + mapped_reads=sample_metrics.mapped_reads / sample_metrics.total_reads, + mean_target_coverage=coverage_metrics.mean_coverage if coverage_metrics else None, + million_read_pairs=get_million_read_pairs(sample.reads), + pct_10x=coverage_metrics.coverage_completeness_percent if coverage_metrics else None, + sex=sample_metrics.predicted_sex_sex_check, + ) + + def is_report_accredited( + self, samples: list[SampleModel], analysis_metadata: AnalysisModel = None + ) -> bool: + """ + Return whether the Raredisease delivery report is accredited. + This method evaluates the accreditation status of each sample's application. + """ + return all(sample.application.accredited for sample in samples) + + def get_scout_uploaded_files(self, case_id: str) -> ScoutReportFiles: + """Return Raredisease files that will be uploaded to Scout.""" + return ScoutReportFiles( + snv_vcf=self.get_scout_uploaded_file_from_hk( + case_id=case_id, scout_key=ScoutUploadKey.SNV_VCF + ), + sv_vcf=self.get_scout_uploaded_file_from_hk( + case_id=case_id, scout_key=ScoutUploadKey.SV_VCF + ), + vcf_str=self.get_scout_uploaded_file_from_hk( + case_id=case_id, scout_key=ScoutUploadKey.VCF_STR + ), + smn_tsv=self.get_scout_uploaded_file_from_hk( + case_id=case_id, scout_key=ScoutUploadKey.SMN_TSV + ), + ) + + @staticmethod + def get_sample_metadata_required_fields(case: CaseModel) -> dict: + """Return sample metadata required fields associated to a specific sample.""" + required_sample_metadata_fields = {} + for sample in case.samples: + required_fields = ( + REQUIRED_SAMPLE_METADATA_RAREDISEASE_WGS_FIELDS + if PrepCategory.WHOLE_GENOME_SEQUENCING.value + in sample.application.prep_category.lower() + else REQUIRED_SAMPLE_METADATA_RAREDISEASE_FIELDS + ) + required_sample_metadata_fields.update({sample.id: required_fields}) + return required_sample_metadata_fields + + def get_required_fields(self, case: CaseModel) -> dict: + """Return dictionary with the delivery report required fields for Raredisease.""" + report_required_fields = ReportRequiredFields( + applications=self.get_application_required_fields( + case=case, required_fields=REQUIRED_APPLICATION_FIELDS + ), + case=REQUIRED_CASE_FIELDS, + customer=REQUIRED_CUSTOMER_FIELDS, + data_analysis=REQUIRED_DATA_ANALYSIS_RAREDISEASE_FIELDS, + metadata=self.get_sample_metadata_required_fields(case=case), + methods=self.get_sample_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_METHODS_FIELDS + ), + report=REQUIRED_REPORT_FIELDS, + samples=self.get_sample_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_RAREDISEASE_FIELDS + ), + timestamps=self.get_timestamp_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_TIMESTAMP_FIELDS + ), + ) + return report_required_fields.model_dump() diff --git a/cg/meta/report/report_api.py b/cg/meta/report/report_api.py index f54f6a1a47..e2603334bc 100644 --- a/cg/meta/report/report_api.py +++ b/cg/meta/report/report_api.py @@ -12,13 +12,11 @@ from cg.constants import DELIVERY_REPORT_FILE_NAME, SWEDAC_LOGO_PATH, Workflow from cg.constants.constants import MAX_ITEMS_TO_RETRIEVE, FileFormat from cg.constants.housekeeper_tags import HK_DELIVERY_REPORT_TAG, HermesFileTag +from cg.constants.scout import ScoutUploadKey from cg.exc import DeliveryReportError from cg.io.controller import ReadFile, WriteStream from cg.meta.meta import MetaAPI -from cg.meta.report.field_validators import ( - get_empty_report_data, - get_missing_report_data, -) +from cg.meta.report.field_validators import get_empty_report_data, get_missing_report_data from cg.meta.workflow.analysis import AnalysisAPI from cg.models.analysis import AnalysisModel from cg.models.cg_config import CGConfig @@ -30,20 +28,8 @@ ReportModel, ScoutReportFiles, ) -from cg.models.report.sample import ( - ApplicationModel, - MethodsModel, - SampleModel, - TimestampModel, -) -from cg.store.models import ( - Analysis, - Application, - ApplicationLimitations, - Case, - CaseSample, - Sample, -) +from cg.models.report.sample import ApplicationModel, MethodsModel, SampleModel, TimestampModel +from cg.store.models import Analysis, Application, ApplicationLimitations, Case, CaseSample, Sample LOG = logging.getLogger(__name__) @@ -108,17 +94,17 @@ def get_delivery_report_from_hk(self, case_id: str, version: Version) -> str | N return None return delivery_report.full_path - def get_scout_uploaded_file_from_hk(self, case_id: str, scout_tag: str) -> str | None: + def get_scout_uploaded_file_from_hk( + self, case_id: str, scout_key: ScoutUploadKey + ) -> str | None: """Return file path of the uploaded to Scout file given its tag.""" version: Version = self.housekeeper_api.last_version(bundle=case_id) - tags: list = self.get_hk_scout_file_tags(scout_tag=scout_tag) + tags: list = self.get_hk_scout_file_tags(scout_key=scout_key) uploaded_file: File = self.housekeeper_api.get_latest_file( bundle=case_id, tags=tags, version=version.id ) if not tags or not uploaded_file: - LOG.warning( - f"No files were found for the following Scout Housekeeper tag: {scout_tag} (case: {case_id})" - ) + LOG.warning(f"No files were found for the following Scout key: {scout_key}") return None return uploaded_file.full_path @@ -273,7 +259,7 @@ def get_samples_data(self, case: Case, analysis_metadata: AnalysisModel) -> list name=sample.name, id=sample.internal_id, ticket=sample.original_ticket, - gender=sample.sex, + sex=sample.sex, source=lims_sample.get("source"), tumour=sample.is_tumour, application=self.get_sample_application(sample=sample, lims_sample=lims_sample), @@ -415,11 +401,7 @@ def get_timestamp_required_fields(case: CaseModel, required_fields: list) -> dic break return ReportAPI.get_sample_required_fields(case=case, required_fields=required_fields) - def get_hk_scout_file_tags(self, scout_tag: str) -> list | None: + def get_hk_scout_file_tags(self, scout_key: ScoutUploadKey) -> list | None: """Return workflow specific uploaded to Scout Housekeeper file tags given a Scout key.""" - tags = self.get_upload_case_tags().get(scout_tag) + tags = self.analysis_api.get_scout_upload_case_tags().get(scout_key.value) return list(tags) if tags else None - - def get_upload_case_tags(self): - """Return workflow specific upload case tags.""" - raise NotImplementedError diff --git a/cg/meta/report/rnafusion.py b/cg/meta/report/rnafusion.py index a95bd21edb..d8b7d25163 100644 --- a/cg/meta/report/rnafusion.py +++ b/cg/meta/report/rnafusion.py @@ -13,7 +13,7 @@ RNAFUSION_REPORT_ACCREDITED_APPTAGS, RNAFUSION_REPORT_MINIMUM_INPUT_AMOUNT, ) -from cg.constants.scout import RNAFUSION_CASE_TAGS +from cg.constants.scout import ScoutUploadKey from cg.meta.report.field_validators import ( get_mapped_reads_fraction, get_million_read_pairs, @@ -23,14 +23,14 @@ from cg.models.analysis import AnalysisModel, NextflowAnalysis from cg.models.cg_config import CGConfig from cg.models.report.metadata import RnafusionSampleMetadataModel -from cg.models.report.report import CaseModel, ScoutReportFiles +from cg.models.report.report import CaseModel, ScoutReportFiles, ReportRequiredFields from cg.models.report.sample import SampleModel from cg.models.rnafusion.rnafusion import RnafusionQCMetrics from cg.store.models import Case, Sample class RnafusionReportAPI(ReportAPI): - """API to create RNAfusion delivery reports.""" + """API to create Rnafusion delivery reports.""" def __init__(self, config: CGConfig, analysis_api: RnafusionAnalysisAPI): super().__init__(config=config, analysis_api=analysis_api) @@ -92,33 +92,32 @@ def is_report_accredited( def get_scout_uploaded_files(self, case_id: str) -> ScoutReportFiles: """Return files that will be uploaded to Scout.""" return ScoutReportFiles( - vcf_fusion=self.get_scout_uploaded_file_from_hk(case_id=case_id, scout_tag="vcf_fusion") + vcf_fusion=self.get_scout_uploaded_file_from_hk( + case_id=case_id, scout_key=ScoutUploadKey.VCF_FUSION + ) ) def get_required_fields(self, case: CaseModel) -> dict: """Return dictionary with the delivery report required fields for Rnafusion.""" - return { - "report": REQUIRED_REPORT_FIELDS, - "customer": REQUIRED_CUSTOMER_FIELDS, - "case": REQUIRED_CASE_FIELDS, - "applications": self.get_application_required_fields( + report_required_fields = ReportRequiredFields( + applications=self.get_application_required_fields( case=case, required_fields=REQUIRED_APPLICATION_FIELDS ), - "data_analysis": REQUIRED_DATA_ANALYSIS_RNAFUSION_FIELDS, - "samples": self.get_sample_required_fields( - case=case, required_fields=REQUIRED_SAMPLE_RNAFUSION_FIELDS + case=REQUIRED_CASE_FIELDS, + customer=REQUIRED_CUSTOMER_FIELDS, + data_analysis=REQUIRED_DATA_ANALYSIS_RNAFUSION_FIELDS, + metadata=self.get_sample_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_METADATA_RNAFUSION_FIELDS ), - "methods": self.get_sample_required_fields( + methods=self.get_sample_required_fields( case=case, required_fields=REQUIRED_SAMPLE_METHODS_FIELDS ), - "timestamps": self.get_timestamp_required_fields( - case=case, required_fields=REQUIRED_SAMPLE_TIMESTAMP_FIELDS + report=REQUIRED_REPORT_FIELDS, + samples=self.get_sample_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_RNAFUSION_FIELDS ), - "metadata": self.get_sample_required_fields( - case=case, required_fields=REQUIRED_SAMPLE_METADATA_RNAFUSION_FIELDS + timestamps=self.get_timestamp_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_TIMESTAMP_FIELDS ), - } - - def get_upload_case_tags(self) -> dict: - """Return Balsamic UMI upload case tags.""" - return RNAFUSION_CASE_TAGS + ) + return report_required_fields.model_dump() diff --git a/cg/meta/report/taxprofiler.py b/cg/meta/report/taxprofiler.py index 996c7751d9..af49463d75 100644 --- a/cg/meta/report/taxprofiler.py +++ b/cg/meta/report/taxprofiler.py @@ -16,7 +16,7 @@ from cg.models.analysis import AnalysisModel, NextflowAnalysis from cg.models.cg_config import CGConfig from cg.models.report.metadata import TaxprofilerSampleMetadataModel -from cg.models.report.report import CaseModel +from cg.models.report.report import CaseModel, ReportRequiredFields from cg.models.report.sample import SampleModel from cg.store.models import Case, Sample @@ -43,24 +43,26 @@ def is_report_accredited( def get_required_fields(self, case: CaseModel) -> dict: """Return the delivery report required fields for Taxprofiler.""" - return { - "report": REQUIRED_REPORT_FIELDS, - "customer": REQUIRED_CUSTOMER_FIELDS, - "case": REQUIRED_CASE_FIELDS, - "applications": self.get_application_required_fields( + + report_required_fields = ReportRequiredFields( + applications=self.get_application_required_fields( case=case, required_fields=REQUIRED_APPLICATION_FIELDS ), - "data_analysis": REQUIRED_DATA_ANALYSIS_TAXPROFILER_FIELDS, - "samples": self.get_sample_required_fields( - case=case, required_fields=REQUIRED_SAMPLE_TAXPROFILER_FIELDS + case=REQUIRED_CASE_FIELDS, + customer=REQUIRED_CUSTOMER_FIELDS, + data_analysis=REQUIRED_DATA_ANALYSIS_TAXPROFILER_FIELDS, + metadata=self.get_sample_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_METADATA_TAXPROFILER_FIELDS ), - "methods": self.get_sample_required_fields( + methods=self.get_sample_required_fields( case=case, required_fields=REQUIRED_SAMPLE_METHODS_FIELDS ), - "timestamps": self.get_timestamp_required_fields( - case=case, required_fields=REQUIRED_SAMPLE_TIMESTAMP_FIELDS + report=REQUIRED_REPORT_FIELDS, + samples=self.get_sample_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_TAXPROFILER_FIELDS ), - "metadata": self.get_sample_required_fields( - case=case, required_fields=REQUIRED_SAMPLE_METADATA_TAXPROFILER_FIELDS + timestamps=self.get_timestamp_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_TIMESTAMP_FIELDS ), - } + ) + return report_required_fields.model_dump() diff --git a/cg/meta/report/templates/macros/data_analysis/data_analysis.html b/cg/meta/report/templates/macros/data_analysis/data_analysis.html index 5b11250a48..f2b3f9c4f4 100644 --- a/cg/meta/report/templates/macros/data_analysis/data_analysis.html +++ b/cg/meta/report/templates/macros/data_analysis/data_analysis.html @@ -1,5 +1,6 @@ {% from "/macros/data_analysis/qc_metrics/balsamic_qc_metrics.html" import balsamic_qc_metrics %} {% from "/macros/data_analysis/qc_metrics/mip_dna_qc_metrics.html" import mip_dna_qc_metrics %} +{% from "/macros/data_analysis/qc_metrics/raredisease_qc_metrics.html" import raredisease_qc_metrics %} {% from "/macros/data_analysis/qc_metrics/rnafusion_qc_metrics.html" import rnafusion_qc_metrics %} {% from "/macros/data_analysis/qc_metrics/tomte_qc_metrics.html" import tomte_qc_metrics %} {% from "/macros/data_analysis/limitations.html" import apptag_limitations %} @@ -16,6 +17,9 @@

Analys

{% elif workflow == "mip-dna" %} {{ mip_dna_qc_metrics(samples=case.samples) }} + {% elif workflow == "raredisease" %} + + {{ raredisease_qc_metrics(samples=case.samples) }} {% elif workflow == "rnafusion" %} {{ rnafusion_qc_metrics(samples=case.samples) }} @@ -40,7 +44,7 @@

Dataanalys

{% if "balsamic" in workflow %} Analystyp {% endif %} {% if "balsamic" in workflow and case.data_analysis.pons != "N/A" %} Panel of Normals {% endif %} {% if workflow in ("balsamic", "balsamic-umi") %} Variantanropare {% endif %} - {% if workflow in ("mip-dna", "rnafusion", "tomte") %} Genpaneler {% endif %} + {% if workflow in ("mip-dna", "raredisease", "rnafusion", "tomte") %} Genpaneler {% endif %} @@ -51,7 +55,7 @@

Dataanalys

{% if "balsamic" in workflow %} {{ case.data_analysis.type }} {% endif %} {% if "balsamic" in workflow and case.data_analysis.pons != "N/A" %} {{ case.data_analysis.pons }} {% endif %} {% if workflow in ("balsamic", "balsamic-umi") %} {{ case.data_analysis.variant_callers }} {% endif %} - {% if workflow in ("mip-dna", "rnafusion", "tomte") %} {{ case.data_analysis.panels }} {% endif %} + {% if workflow in ("mip-dna", "raredisease", "rnafusion", "tomte") %} {{ case.data_analysis.panels }} {% endif %} diff --git a/cg/meta/report/templates/macros/data_analysis/qc_metrics/mip_dna_qc_metrics.html b/cg/meta/report/templates/macros/data_analysis/qc_metrics/mip_dna_qc_metrics.html index 544c0ef8dc..02dfbc90f7 100644 --- a/cg/meta/report/templates/macros/data_analysis/qc_metrics/mip_dna_qc_metrics.html +++ b/cg/meta/report/templates/macros/data_analysis/qc_metrics/mip_dna_qc_metrics.html @@ -3,7 +3,7 @@ {% macro mip_dna_qc_metrics(samples) %} {% set metrics = [ - {"name": "Kön", "key": "gender", "description": "Kön beräknat genom bioinformatisk analys."}, + {"name": "Kön", "key": "sex", "description": "Kön beräknat genom bioinformatisk analys."}, {"name": "Läspar [M]", "key": "million_read_pairs", "description": "Antal sekvenseringsläsningar i miljoner läspar."}, {"name": "Mappade sekvenser [%]", "key": "mapped_reads", "description": "Procent sekvenser som matchar en eller flera positioner i referensgenomet."}, {"name": "Medelsekvensdjup", "key": "mean_target_coverage", "description": "Medelvärdet av täckningsgraden i baser över genpanelen/genpanelerna angivna under dataanalys."}, diff --git a/cg/meta/report/templates/macros/data_analysis/qc_metrics/raredisease_qc_metrics.html b/cg/meta/report/templates/macros/data_analysis/qc_metrics/raredisease_qc_metrics.html new file mode 100644 index 0000000000..b05aa89cbb --- /dev/null +++ b/cg/meta/report/templates/macros/data_analysis/qc_metrics/raredisease_qc_metrics.html @@ -0,0 +1,16 @@ +{% from "/macros/data_analysis/qc_metrics/qc_metrics.html" import qc_metrics %} + +{% macro raredisease_qc_metrics(samples) %} + {% set + metrics = [ + {"name": "Kön", "key": "sex", "description": "Kön beräknat genom bioinformatisk analys."}, + {"name": "Läspar [M]", "key": "million_read_pairs", "description": "Antal sekvenseringsläsningar i miljoner läspar."}, + {"name": "Mappade sekvenser [%]", "key": "mapped_reads", "description": "Procent sekvenser som matchar en eller flera positioner i referensgenomet."}, + {"name": "Medelsekvensdjup", "key": "mean_target_coverage", "description": "Medelvärdet av täckningsgraden i baser över genpanelen/genpanelerna angivna under dataanalys."}, + {"name": "Täckningsgrad 10x [%]", "key": "pct_10x", "description": "Andel baser som är sekvenserade med ett djup över en specificerad gräns (10x) över genpanelen/genpanelerna angivna under dataanalys. Det beräknas efter borttagning av duplikata läsningar."}, + {"name": "Duplikat [%]", "key": "duplicates", "description": "Sekvenseringsläsningar som är duplikat (kopior) och därmed ej unika sekvenser. Hög mängd duplikat kan tyda på dålig komplexitet av sekvenserat bibliotek eller djup sekvensering."}, + ] + %} + + {{ qc_metrics(samples=samples, metrics=metrics) }} +{% endmacro %} diff --git a/cg/meta/report/templates/macros/order.html b/cg/meta/report/templates/macros/order.html index 9199fb21c1..22b95144fc 100644 --- a/cg/meta/report/templates/macros/order.html +++ b/cg/meta/report/templates/macros/order.html @@ -32,7 +32,7 @@

Beställning

{{ sample.timestamps.ordered_at }} {{ sample.ticket }} {{ sample.application.tag }} (v{{ sample.application.version }}) - {% if customer_workflow != "taxprofiler" %} {{ sample.gender }} {% endif %} + {% if customer_workflow != "taxprofiler" %} {{ sample.sex }} {% endif %} {{ sample.source }} {{ sample_status_value(workflow=customer_workflow, sample=sample) }} @@ -46,7 +46,7 @@

Beställning

{% macro sample_status(workflow) %} {% if "balsamic" in workflow or workflow == "rnafusion" %} Tumör - {% elif workflow in ("mip-dna", "tomte") %} + {% elif workflow in ("mip-dna", "raredisease", "tomte") %} Status {% endif %} {% endmacro %} @@ -54,7 +54,7 @@

Beställning

{% macro sample_status_value(workflow, sample) %} {% if "balsamic" in workflow or workflow == "rnafusion" %} {{ sample.tumour }} - {% elif workflow in ("mip-dna", "tomte") %} + {% elif workflow in ("mip-dna", "raredisease", "tomte") %} {{ sample.status }} {% endif %} {% endmacro %} diff --git a/cg/meta/report/templates/macros/sample_prep.html b/cg/meta/report/templates/macros/sample_prep.html index d9f5ebdee9..2056d0d6cb 100644 --- a/cg/meta/report/templates/macros/sample_prep.html +++ b/cg/meta/report/templates/macros/sample_prep.html @@ -46,7 +46,7 @@

Provberedning

{% macro bait_set(workflow, analysis_type) %} {% if ("balsamic" in workflow and "panelsekvensering" in analysis_type) or - (workflow == "mip-dna" and analysis_type == "wes") + (workflow in ("mip-dna", "raredisease") and analysis_type == "wes") %} Bait Set {% endif %} @@ -55,7 +55,7 @@

Provberedning

{% macro bait_set_value(workflow, analysis_type, sample) %} {% if ("balsamic" in workflow and "panelsekvensering" in analysis_type) or - (workflow == "mip-dna" and analysis_type == "wes") + (workflow in ("mip-dna", "raredisease") and analysis_type == "wes") %} {{ sample.metadata.bait_set }} {% if "balsamic" in workflow and sample.metadata.bait_set != "N/A" %} diff --git a/cg/meta/report/templates/macros/uploaded_files/raredisease_uploaded_files.html b/cg/meta/report/templates/macros/uploaded_files/raredisease_uploaded_files.html new file mode 100644 index 0000000000..ccc8804456 --- /dev/null +++ b/cg/meta/report/templates/macros/uploaded_files/raredisease_uploaded_files.html @@ -0,0 +1,37 @@ +{% macro raredisease_scout_files(scout_files, case_id, case_name) %} + + + {% if scout_files.snv_vcf != "N/A" %} + + + + + {% endif %} {% if scout_files.snv_research_vcf != "N/A" %} + + + + + {% endif %} {% if scout_files.sv_vcf != "N/A" %} + + + + + {% endif %} {% if scout_files.sv_research_vcf != "N/A" %} + + + + + {% endif %} {% if scout_files.vcf_str != "N/A" %} + + + + + {% endif %} {% if scout_files.smn_tsv != "N/A" %} + + + + + {% endif %} + +
Kliniskt relevanta SNVs och INDELs{{ scout_files.snv_vcf.replace(case_id, case_name) }}
SNVs och INDELs för forskning{{ scout_files.snv_research_vcf.replace(case_id, case_name) }}
Kliniskt relevanta SVs{{ scout_files.sv_vcf.replace(case_id, case_name) }}
SVs för forskning{{ scout_files.sv_research_vcf.replace(case_id, case_name) }}
Kliniskt relevanta STRs{{ scout_files.vcf_str.replace(case_id, case_name) }}
SMN CNVs{{ scout_files.smn_tsv.replace(case_id, case_name) }}
+{% endmacro %} diff --git a/cg/meta/report/templates/macros/uploaded_files/uploaded_files.html b/cg/meta/report/templates/macros/uploaded_files/uploaded_files.html index e1c6292087..2ab1654188 100644 --- a/cg/meta/report/templates/macros/uploaded_files/uploaded_files.html +++ b/cg/meta/report/templates/macros/uploaded_files/uploaded_files.html @@ -1,5 +1,6 @@ {% from "/macros/uploaded_files/balsamic_uploaded_files.html" import balsamic_scout_files %} {% from "/macros/uploaded_files/mip_dna_uploaded_files.html" import mip_dna_scout_files %} +{% from "/macros/uploaded_files/raredisease_uploaded_files.html" import raredisease_scout_files %} {% from "/macros/uploaded_files/rnafusion_uploaded_files.html" import rnafusion_scout_files %} {% macro uploaded_files(case, customer) %} @@ -35,6 +36,9 @@

Scout

{% elif workflow == "mip-dna" %} {{ mip_dna_scout_files(scout_files=case.data_analysis.scout_files, case_id=case.id, case_name=case.name) }} + {% elif workflow == "raredisease" %} + + {{ raredisease_scout_files(scout_files=case.data_analysis.scout_files, case_id=case.id, case_name=case.name) }} {% elif workflow == "rnafusion" %} {{ rnafusion_scout_files(scout_files=case.data_analysis.scout_files, case_id=case.id, case_name=case.name) }} diff --git a/cg/meta/report/tomte.py b/cg/meta/report/tomte.py index b0cc9f464c..f4a654f6e1 100644 --- a/cg/meta/report/tomte.py +++ b/cg/meta/report/tomte.py @@ -17,7 +17,7 @@ from cg.models.analysis import AnalysisModel, NextflowAnalysis from cg.models.cg_config import CGConfig from cg.models.report.metadata import TomteSampleMetadataModel -from cg.models.report.report import CaseModel +from cg.models.report.report import CaseModel, ReportRequiredFields from cg.models.report.sample import SampleModel from cg.models.tomte.tomte import TomteQCMetrics from cg.store.models import Case, Sample @@ -63,24 +63,25 @@ def is_report_accredited( def get_required_fields(self, case: CaseModel) -> dict: """Return the delivery report required fields for Tomte.""" - return { - "report": REQUIRED_REPORT_FIELDS, - "customer": REQUIRED_CUSTOMER_FIELDS, - "case": REQUIRED_CASE_FIELDS, - "applications": self.get_application_required_fields( + report_required_fields = ReportRequiredFields( + applications=self.get_application_required_fields( case=case, required_fields=REQUIRED_APPLICATION_FIELDS ), - "data_analysis": REQUIRED_DATA_ANALYSIS_TOMTE_FIELDS, - "samples": self.get_sample_required_fields( - case=case, required_fields=REQUIRED_SAMPLE_TOMTE_FIELDS + case=REQUIRED_CASE_FIELDS, + customer=REQUIRED_CUSTOMER_FIELDS, + data_analysis=REQUIRED_DATA_ANALYSIS_TOMTE_FIELDS, + metadata=self.get_sample_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_METADATA_TOMTE_FIELDS ), - "methods": self.get_sample_required_fields( + methods=self.get_sample_required_fields( case=case, required_fields=REQUIRED_SAMPLE_METHODS_FIELDS ), - "timestamps": self.get_timestamp_required_fields( - case=case, required_fields=REQUIRED_SAMPLE_TIMESTAMP_FIELDS + report=REQUIRED_REPORT_FIELDS, + samples=self.get_sample_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_TOMTE_FIELDS ), - "metadata": self.get_sample_required_fields( - case=case, required_fields=REQUIRED_SAMPLE_METADATA_TOMTE_FIELDS + timestamps=self.get_timestamp_required_fields( + case=case, required_fields=REQUIRED_SAMPLE_TIMESTAMP_FIELDS ), - } + ) + return report_required_fields.model_dump() diff --git a/cg/meta/workflow/analysis.py b/cg/meta/workflow/analysis.py index 4185707aac..922cee01c6 100644 --- a/cg/meta/workflow/analysis.py +++ b/cg/meta/workflow/analysis.py @@ -10,15 +10,17 @@ from housekeeper.store.models import Bundle, Version from cg.apps.environ import environ_email +from cg.clients.chanjo2.models import CoverageMetrics from cg.constants import EXIT_FAIL, EXIT_SUCCESS, Priority, SequencingFileTag, Workflow from cg.constants.constants import ( AnalysisType, CaseActions, FileFormat, + GenomeVersion, WorkflowManager, ) from cg.constants.gene_panel import GenePanelCombo, GenePanelMasterList -from cg.constants.scout import ScoutExportFileName +from cg.constants.scout import ScoutExportFileName, HGNC_ID from cg.constants.tb import AnalysisStatus from cg.exc import AnalysisNotReadyError, BundleAlreadyAddedError, CgDataError, CgError from cg.io.controller import WriteFile @@ -729,3 +731,32 @@ def get_data_analysis_type(self, case_id: str) -> str | None: f"Case samples have different analysis types {', '.join(analysis_types)}" ) return analysis_types.pop() if analysis_types else None + + @staticmethod + def translate_genome_reference(genome_version: GenomeVersion) -> GenomeVersion: + """Translates a genome reference assembly to its corresponding alternate name.""" + translation_map = { + GenomeVersion.GRCh37: GenomeVersion.HG19, + GenomeVersion.GRCh38: GenomeVersion.HG38, + GenomeVersion.HG19: GenomeVersion.GRCh37, + GenomeVersion.HG38: GenomeVersion.GRCh38, + } + return translation_map.get(genome_version, genome_version) + + def get_sample_coverage( + self, case_id: str, sample_id: str, gene_ids: list[int] + ) -> CoverageMetrics | None: + """Return sample coverage data from Chanjo2.""" + raise NotImplementedError + + def get_scout_upload_case_tags(self): + """Return workflow specific upload case tags.""" + raise NotImplementedError + + def get_gene_ids_from_scout(self, panels: list[str]) -> list[int]: + """Return HGNC IDs of genes from specified panels using the Scout API.""" + gene_ids: list[int] = [] + for panel in panels: + genes: list[dict] = self.scout_api.get_genes(panel) + gene_ids.extend(gene.get(HGNC_ID) for gene in genes if gene.get(HGNC_ID) is not None) + return gene_ids diff --git a/cg/meta/workflow/balsamic.py b/cg/meta/workflow/balsamic.py index 071bba0fe8..20a915d989 100644 --- a/cg/meta/workflow/balsamic.py +++ b/cg/meta/workflow/balsamic.py @@ -11,6 +11,7 @@ from cg.constants.housekeeper_tags import BalsamicAnalysisTag from cg.constants.observations import ObservationsFileWildcards from cg.constants.priority import SlurmQos +from cg.constants.scout import BALSAMIC_CASE_TAGS from cg.constants.sequencing import Variants from cg.constants.subject import Sex from cg.exc import BalsamicStartError, CgError @@ -667,3 +668,7 @@ def is_analysis_normal_only(self, case_id: str) -> bool: if case.non_tumour_samples and not case.tumour_samples: return True return False + + def get_scout_upload_case_tags(self) -> dict: + """Return Balsamic Scout upload case tags.""" + return BALSAMIC_CASE_TAGS diff --git a/cg/meta/workflow/balsamic_umi.py b/cg/meta/workflow/balsamic_umi.py index e04033925d..7785806b80 100644 --- a/cg/meta/workflow/balsamic_umi.py +++ b/cg/meta/workflow/balsamic_umi.py @@ -3,6 +3,7 @@ import logging from cg.constants import Workflow +from cg.constants.scout import BALSAMIC_UMI_CASE_TAGS from cg.meta.workflow.balsamic import BalsamicAnalysisAPI from cg.models.cg_config import CGConfig @@ -19,3 +20,7 @@ def __init__( workflow: Workflow = Workflow.BALSAMIC_UMI, ): super().__init__(config=config, workflow=workflow) + + def get_scout_upload_case_tags(self) -> dict: + """Return Balsamic UMI Scout upload case tags.""" + return BALSAMIC_UMI_CASE_TAGS diff --git a/cg/meta/workflow/mip_dna.py b/cg/meta/workflow/mip_dna.py index b96dc6aae5..c5090509d7 100644 --- a/cg/meta/workflow/mip_dna.py +++ b/cg/meta/workflow/mip_dna.py @@ -4,10 +4,11 @@ from cg.constants.constants import AnalysisType from cg.constants.gene_panel import GENOME_BUILD_37 from cg.constants.pedigree import Pedigree +from cg.constants.scout import MIP_CASE_TAGS from cg.meta.workflow.mip import MipAnalysisAPI from cg.models.cg_config import CGConfig from cg.models.mip.mip_analysis import MipAnalysis -from cg.store.models import CaseSample, Case +from cg.store.models import Case, CaseSample from cg.utils import Process LOG = logging.getLogger(__name__) @@ -94,3 +95,7 @@ def get_data_analysis_type(self, case_id: str) -> str: ) return AnalysisType.WHOLE_GENOME_SEQUENCING return analysis_types.pop() if analysis_types else None + + def get_scout_upload_case_tags(self) -> dict: + """Return MIP DNA Scout upload case tags.""" + return MIP_CASE_TAGS diff --git a/cg/meta/workflow/raredisease.py b/cg/meta/workflow/raredisease.py index 74da2d0ff3..72b6a50b04 100644 --- a/cg/meta/workflow/raredisease.py +++ b/cg/meta/workflow/raredisease.py @@ -4,10 +4,24 @@ from pathlib import Path from typing import Any +from housekeeper.store.models import File + +from cg.clients.chanjo2.models import ( + CoverageMetrics, + CoveragePostRequest, + CoveragePostResponse, + CoverageSample, +) from cg.constants import DEFAULT_CAPTURE_KIT, Workflow from cg.constants.constants import AnalysisType, GenomeVersion from cg.constants.gene_panel import GenePanelGenomeBuild -from cg.constants.nf_analysis import RAREDISEASE_METRIC_CONDITIONS +from cg.constants.nf_analysis import ( + RAREDISEASE_METRIC_CONDITIONS, + RAREDISEASE_COVERAGE_FILE_TAGS, + RAREDISEASE_COVERAGE_THRESHOLD, + RAREDISEASE_COVERAGE_INTERVAL_TYPE, +) +from cg.constants.scout import RAREDISEASE_CASE_TAGS from cg.constants.subject import PlinkPhenotypeStatus, PlinkSex from cg.meta.workflow.nf_analysis import NfAnalysisAPI from cg.models.cg_config import CGConfig @@ -54,9 +68,9 @@ def sample_sheet_headers(self) -> list[str]: """Headers for sample sheet.""" return RarediseaseSampleSheetHeaders.list() - def get_genome_build(self, case_id: str) -> GenomeVersion: + def get_genome_build(self, case_id: str | None = None) -> GenomeVersion: """Return reference genome for a case. Currently fixed for hg19.""" - return GenomeVersion.hg19 + return GenomeVersion.HG19 def get_sample_sheet_content_per_sample(self, case_sample: CaseSample) -> list[list[str]]: """Collect and format information required to build a sample sheet for a single sample.""" @@ -142,3 +156,40 @@ def get_workflow_metrics(self, sample_id: str) -> dict: @staticmethod def set_order_sex_for_sample(sample: Sample, metric_conditions: dict) -> None: metric_conditions["predicted_sex_sex_check"]["threshold"] = sample.sex + + def get_sample_coverage_file_path(self, bundle_name: str, sample_id: str) -> str | None: + """Return the Raredisease d4 coverage file path.""" + coverage_file_tags: list[str] = RAREDISEASE_COVERAGE_FILE_TAGS + [sample_id] + coverage_file: File | None = self.housekeeper_api.get_file_from_latest_version( + bundle_name=bundle_name, tags=coverage_file_tags + ) + if coverage_file: + return coverage_file.full_path + LOG.warning(f"No coverage file found with the tags: {coverage_file_tags}") + return None + + def get_sample_coverage( + self, case_id: str, sample_id: str, gene_ids: list[int] + ) -> CoverageMetrics | None: + """Return sample coverage metrics from Chanjo2.""" + genome_version: GenomeVersion = self.get_genome_build() + coverage_file_path: str | None = self.get_sample_coverage_file_path( + bundle_name=case_id, sample_id=sample_id + ) + try: + post_request = CoveragePostRequest( + build=self.translate_genome_reference(genome_version), + coverage_threshold=RAREDISEASE_COVERAGE_THRESHOLD, + hgnc_gene_ids=gene_ids, + interval_type=RAREDISEASE_COVERAGE_INTERVAL_TYPE, + samples=[CoverageSample(coverage_file_path=coverage_file_path, name=sample_id)], + ) + post_response: CoveragePostResponse = self.chanjo2_api.get_coverage(post_request) + return post_response.get_sample_coverage_metrics(sample_id) + except Exception as error: + LOG.error(f"Error getting coverage for sample '{sample_id}', error: {error}") + return None + + def get_scout_upload_case_tags(self) -> dict: + """Return Raredisease Scout upload case tags.""" + return RAREDISEASE_CASE_TAGS diff --git a/cg/meta/workflow/rnafusion.py b/cg/meta/workflow/rnafusion.py index 50c93fb837..520b3687dd 100644 --- a/cg/meta/workflow/rnafusion.py +++ b/cg/meta/workflow/rnafusion.py @@ -6,9 +6,9 @@ from cg.constants import Workflow from cg.constants.constants import GenomeVersion, Strandedness from cg.constants.nf_analysis import MULTIQC_NEXFLOW_CONFIG, RNAFUSION_METRIC_CONDITIONS +from cg.constants.scout import RNAFUSION_CASE_TAGS from cg.exc import MissingMetrics from cg.meta.workflow.nf_analysis import NfAnalysisAPI -from cg.models.analysis import AnalysisModel from cg.models.cg_config import CGConfig from cg.models.deliverables.metric_deliverables import MetricsBase from cg.models.rnafusion.rnafusion import RnafusionParameters, RnafusionSampleSheetEntry @@ -59,7 +59,7 @@ def is_multiple_samples_allowed(self) -> bool: def get_genome_build(self, case_id: str) -> GenomeVersion: """Return reference genome for a case. Currently fixed for hg38.""" - return GenomeVersion.hg38 + return GenomeVersion.HG38 def get_nextflow_config_content(self, case_id: str) -> str: """Return nextflow config content.""" @@ -122,3 +122,7 @@ def ensure_mandatory_metrics_present(metrics: list[MetricsBase]) -> None: def get_workflow_metrics(self, metric_id: str) -> dict: return RNAFUSION_METRIC_CONDITIONS + + def get_scout_upload_case_tags(self) -> dict: + """Return Rnafusion Scout upload case tags.""" + return RNAFUSION_CASE_TAGS diff --git a/cg/models/analysis.py b/cg/models/analysis.py index 57519460d4..c6367b5020 100644 --- a/cg/models/analysis.py +++ b/cg/models/analysis.py @@ -1,5 +1,6 @@ from pydantic.v1 import BaseModel +from cg.models.raredisease.raredisease import RarediseaseQCMetrics from cg.models.rnafusion.rnafusion import RnafusionQCMetrics from cg.models.taxprofiler.taxprofiler import TaxprofilerQCMetrics from cg.models.tomte.tomte import TomteQCMetrics @@ -12,4 +13,6 @@ class AnalysisModel(BaseModel): class NextflowAnalysis(AnalysisModel): """Nextflow's analysis results model.""" - sample_metrics: dict[str, RnafusionQCMetrics | TaxprofilerQCMetrics | TomteQCMetrics] + sample_metrics: dict[ + str, RarediseaseQCMetrics | RnafusionQCMetrics | TaxprofilerQCMetrics | TomteQCMetrics + ] diff --git a/cg/models/raredisease/raredisease.py b/cg/models/raredisease/raredisease.py index 681e56450c..12f4ab9d95 100644 --- a/cg/models/raredisease/raredisease.py +++ b/cg/models/raredisease/raredisease.py @@ -1,20 +1,17 @@ from enum import StrEnum -from pydantic import BaseModel - +from cg.constants.constants import SexOptions from cg.models.nf_analysis import NextflowSampleSheetEntry, WorkflowParameters +from cg.models.qc_metrics import QCMetrics -class RarediseaseQCMetrics(BaseModel): - """Raredisease QC metrics""" +class RarediseaseQCMetrics(QCMetrics): + """Raredisease QC metrics.""" - percentage_mapped_reads: float | None - pct_target_bases_10x: float | None - median_target_coverage: float | None - pct_pf_reads_aligned: float | None - pct_pf_reads_improper_pairs: float | None - pct_adapter: float | None - fraction_duplicates: float | None + mapped_reads: int + percent_duplicates: float + predicted_sex_sex_check: SexOptions + total_reads: int class RarediseaseSampleSheetEntry(NextflowSampleSheetEntry): diff --git a/cg/models/report/metadata.py b/cg/models/report/metadata.py index 5ca5fc50d9..14b45551d1 100644 --- a/cg/models/report/metadata.py +++ b/cg/models/report/metadata.py @@ -31,17 +31,35 @@ class MipDNASampleMetadataModel(SampleMetadataModel): Attributes: bait_set: panel bed used for the analysis; source: LIMS - gender: gender estimated by the workflow; source: workflow mapped_reads: percentage of reads aligned to the reference sequence; source: workflow mean_target_coverage: mean coverage of a target region; source: workflow pct_10x: percent of targeted bases that are covered to 10X coverage or more; source: workflow + sex: sex predicted by the workflow; source: workflow """ bait_set: Annotated[str, BeforeValidator(get_report_string)] = NA_FIELD - gender: Annotated[str, BeforeValidator(get_sex_as_string)] = NA_FIELD mapped_reads: Annotated[str, BeforeValidator(get_number_as_string)] = NA_FIELD mean_target_coverage: Annotated[str, BeforeValidator(get_number_as_string)] = NA_FIELD pct_10x: Annotated[str, BeforeValidator(get_number_as_string)] = NA_FIELD + sex: Annotated[str, BeforeValidator(get_sex_as_string)] = NA_FIELD + + +class RarediseaseSampleMetadataModel(SampleMetadataModel): + """Metrics and trending data model associated to a specific MIP DNA sample. + + Attributes: + bait_set: panel bed used for the analysis; source: LIMS + mapped_reads: percentage of reads aligned to the reference sequence; source: workflow + mean_target_coverage: mean coverage of a target region; source: Chanjo2 + pct_10x: percent of targeted bases that are covered to 10X coverage or more; source: Chanjo2 + sex: sex predicted by the workflow; source: workflow + """ + + bait_set: Annotated[str, BeforeValidator(get_report_string)] = NA_FIELD + mapped_reads: Annotated[str, BeforeValidator(get_number_as_string)] = NA_FIELD + mean_target_coverage: Annotated[str, BeforeValidator(get_number_as_string)] = NA_FIELD + pct_10x: Annotated[str, BeforeValidator(get_number_as_string)] = NA_FIELD + sex: Annotated[str, BeforeValidator(get_sex_as_string)] = NA_FIELD class BalsamicSampleMetadataModel(SampleMetadataModel): diff --git a/cg/models/report/report.py b/cg/models/report/report.py index e6bb913567..4dd8028827 100644 --- a/cg/models/report/report.py +++ b/cg/models/report/report.py @@ -140,3 +140,17 @@ class ReportModel(BaseModel): date: Annotated[str, BeforeValidator(get_date_as_string)] = NA_FIELD case: CaseModel accredited: bool | None = None + + +class ReportRequiredFields(BaseModel): + """Model that defines the mandatory fields of the different sections of the delivery report.""" + + applications: dict[str, list[str]] + case: list[str] + customer: list[str] + data_analysis: list[str] + metadata: dict[str, list[str]] + methods: dict[str, list[str]] + report: list[str] + samples: dict[str, list[str]] + timestamps: dict[str, list[str]] diff --git a/cg/models/report/sample.py b/cg/models/report/sample.py index 681431c36a..c13c055f2a 100644 --- a/cg/models/report/sample.py +++ b/cg/models/report/sample.py @@ -6,6 +6,7 @@ BalsamicTargetedSampleMetadataModel, BalsamicWGSSampleMetadataModel, MipDNASampleMetadataModel, + RarediseaseSampleMetadataModel, RnafusionSampleMetadataModel, TaxprofilerSampleMetadataModel, TomteSampleMetadataModel, @@ -84,7 +85,7 @@ class SampleModel(BaseModel): id: sample internal ID; source: StatusDB/sample/internal_id ticket: ticket number; source: StatusDB/sample/ticket_number status: sample status provided by the customer; source: StatusDB/family-sample/status - gender: sample gender provided by the customer; source: StatusDB/sample/sex + sex: sample sex provided by the customer; source: StatusDB/sample/sex source: sample type/source; source: LIMS/sample/source tumour: whether the sample is a tumour or normal one; source: StatusDB/sample/is_tumour application: analysis application model @@ -99,15 +100,16 @@ class SampleModel(BaseModel): id: Annotated[str, BeforeValidator(get_report_string)] = NA_FIELD ticket: Annotated[str, BeforeValidator(get_report_string)] = NA_FIELD status: Annotated[str, BeforeValidator(get_report_string)] = NA_FIELD - gender: Annotated[str, BeforeValidator(get_sex_as_string)] = NA_FIELD + sex: Annotated[str, BeforeValidator(get_sex_as_string)] = NA_FIELD source: Annotated[str, BeforeValidator(get_report_string)] = NA_FIELD tumour: Annotated[str, BeforeValidator(get_boolean_as_string)] = NA_FIELD application: ApplicationModel methods: MethodsModel metadata: ( - MipDNASampleMetadataModel - | BalsamicTargetedSampleMetadataModel + BalsamicTargetedSampleMetadataModel | BalsamicWGSSampleMetadataModel + | MipDNASampleMetadataModel + | RarediseaseSampleMetadataModel | RnafusionSampleMetadataModel | TaxprofilerSampleMetadataModel | TomteSampleMetadataModel diff --git a/cg/models/tomte/tomte.py b/cg/models/tomte/tomte.py index 1f491f1df4..67746011b1 100644 --- a/cg/models/tomte/tomte.py +++ b/cg/models/tomte/tomte.py @@ -62,10 +62,10 @@ def restrict_tissue_values(cls, tissue: str | None) -> str: @validator("genome", pre=True) def restrict_genome_values(cls, genome: str) -> str: - if genome == GenomeVersion.hg38: - return "GRCh38" - elif genome == GenomeVersion.hg19: - return "GRCh37" + if genome == GenomeVersion.HG38: + return GenomeVersion.GRCh38.value + elif genome == GenomeVersion.HG19: + return GenomeVersion.GRCh37.value class TomteQCMetrics(QCMetrics): diff --git a/tests/clients/chanjo2/test_chanjo2_client.py b/tests/clients/chanjo2/test_chanjo2_client.py index 9a3b549249..8c0ceec7f0 100644 --- a/tests/clients/chanjo2/test_chanjo2_client.py +++ b/tests/clients/chanjo2/test_chanjo2_client.py @@ -7,7 +7,7 @@ from pytest_mock import MockFixture from cg.clients.chanjo2.client import Chanjo2APIClient -from cg.clients.chanjo2.models import CoveragePostRequest, CoveragePostResponse +from cg.clients.chanjo2.models import CoverageMetrics, CoveragePostRequest, CoveragePostResponse from cg.exc import Chanjo2RequestError, Chanjo2ResponseError @@ -100,3 +100,35 @@ def test_get_coverage_empty_response( # THEN a Chanjo2 response error should have been raised with pytest.raises(Chanjo2ResponseError): chanjo2_api_client.get_coverage(coverage_post_request) + + +def test_get_sample_coverage_metrics( + sample_id: str, + coverage_post_response: CoveragePostResponse, + coverage_post_response_json: dict[str, dict], +): + """Test sample coverage extraction from a coverage POST response.""" + + # GIVEN a mocked POST response + + # WHEN getting the coverage data for a specific sample ID + coverage_metrics: CoverageMetrics = coverage_post_response.get_sample_coverage_metrics( + sample_id + ) + + # THEN the returned coverage metrics should match the expected one + assert coverage_metrics.model_dump() == coverage_post_response_json[sample_id] + + +def test_get_sample_coverage_metrics_invalid_sample_id( + invalid_sample_id: str, coverage_post_response: CoveragePostResponse +): + """Test sample coverage extraction from a coverage POST response providing an invalid sample.""" + + # GIVEN a mocked POST response + + # WHEN getting the coverage data for a specific sample ID + + # THEN a validation error should be raised + with pytest.raises(ValueError): + coverage_post_response.get_sample_coverage_metrics(invalid_sample_id) diff --git a/tests/conftest.py b/tests/conftest.py index 8d8ea2ce5b..8d5838b36e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3410,7 +3410,7 @@ def tomte_context( internal_id=sample_id, reads=total_sequenced_reads_pass, last_sequenced_at=datetime.now(), - reference_genome=GenomeVersion.hg38, + reference_genome=GenomeVersion.HG38, ) helpers.add_relationship( diff --git a/tests/meta/report/test_mip_dna_api.py b/tests/meta/report/test_mip_dna_api.py index 65c4bb83f8..678a136943 100644 --- a/tests/meta/report/test_mip_dna_api.py +++ b/tests/meta/report/test_mip_dna_api.py @@ -18,7 +18,7 @@ def test_get_sample_metadata( expected_metadata = { "bait_set": NA_FIELD, "duplicates": "4.01", - "gender": REPORT_SEX.get(Sex.MALE), + "sex": REPORT_SEX.get(Sex.MALE), "initial_qc": REPORT_QC_FLAG.get(True), "mapped_reads": "99.77", "mean_target_coverage": "38.34", diff --git a/tests/meta/report/test_report_api.py b/tests/meta/report/test_report_api.py index d993b38eab..fc952a2e39 100644 --- a/tests/meta/report/test_report_api.py +++ b/tests/meta/report/test_report_api.py @@ -269,7 +269,7 @@ def test_get_samples_data( assert samples_data.id == str(expected_sample_data.sample.internal_id) assert samples_data.ticket == str(expected_sample_data.sample.original_ticket) assert samples_data.status == str(expected_sample_data.status) - assert samples_data.gender == REPORT_SEX.get(str(expected_sample_data.sample.sex)) + assert samples_data.sex == REPORT_SEX.get(str(expected_sample_data.sample.sex)) assert samples_data.source == str(expected_lims_data.get("source")) assert samples_data.tumour == "Nej" assert samples_data.application diff --git a/tests/mocks/report.py b/tests/mocks/report.py index 7524a9151b..ef40fc4013 100644 --- a/tests/mocks/report.py +++ b/tests/mocks/report.py @@ -8,6 +8,7 @@ from cg.apps.coverage import ChanjoAPI from cg.constants.constants import AnalysisType, GenomeVersion +from cg.constants.scout import ScoutUploadKey from cg.meta.report.mip_dna import MipDNAReportAPI from cg.meta.workflow.analysis import AnalysisAPI from cg.meta.workflow.mip_dna import MipDNAAnalysisAPI @@ -35,7 +36,7 @@ def get_latest_metadata(family_id: str = None, **kwargs) -> MipAnalysis: metrics: MIPMetricsDeliverables = create_mip_metrics_deliverables() return MipAnalysis( case=family_id or "yellowhog", - genome_build=GenomeVersion.hg19.value, + genome_build=GenomeVersion.HG19.value, sample_id_metrics=metrics.sample_id_metrics, mip_version="v4.0.20", rank_model_version="1.18", @@ -70,12 +71,12 @@ def get_delivery_report_from_hk(self, case_id: str, version: Version) -> None: ) return None - def get_scout_uploaded_file_from_hk(self, case_id: str, scout_tag: str) -> str: + def get_scout_uploaded_file_from_hk(self, case_id: str, scout_key: ScoutUploadKey) -> str: """Return mocked uploaded to Scout file.""" LOG.info( - f"get_scout_uploaded_file_from_hk called with the following args: case={case_id}, scout_tag={scout_tag}" + f"get_scout_uploaded_file_from_hk called with the following args: case={case_id}, scout_key={scout_key}" ) - return f"path/to/{scout_tag}" + return f"path/to/{scout_key}" class MockMipDNAReportAPI(MockHousekeeperMipDNAReportAPI):