From af2b6ee36af8d482e2dcac067ac743a93bab9317 Mon Sep 17 00:00:00 2001 From: Sebastian Diaz Date: Wed, 29 Jan 2025 11:31:26 +0100 Subject: [PATCH 01/20] add scout delivery types to tomte (#4167)(patch) ## Description Solves a bug in the order flow in which customers can't order scout in tomte ### Added - add scout delivery types to tomte --- cg/services/orders/validation/workflows/tomte/constants.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cg/services/orders/validation/workflows/tomte/constants.py b/cg/services/orders/validation/workflows/tomte/constants.py index 597536e16e..74aa92b35b 100644 --- a/cg/services/orders/validation/workflows/tomte/constants.py +++ b/cg/services/orders/validation/workflows/tomte/constants.py @@ -5,6 +5,10 @@ class TomteDeliveryType(StrEnum): ANALYSIS_FILES = DataDelivery.ANALYSIS_FILES + ANALYSIS_SCOUT = DataDelivery.ANALYSIS_SCOUT FASTQ = DataDelivery.FASTQ FASTQ_ANALYSIS = DataDelivery.FASTQ_ANALYSIS + FASTQ_ANALYSIS_SCOUT = DataDelivery.FASTQ_ANALYSIS_SCOUT + FASTQ_SCOUT = DataDelivery.FASTQ_SCOUT NO_DELIVERY = DataDelivery.NO_DELIVERY + SCOUT = DataDelivery.SCOUT From f0899fb0741712668c138312079e0c8390e429d0 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Wed, 29 Jan 2025 10:31:56 +0000 Subject: [PATCH 02/20] =?UTF-8?q?Bump=20version:=2067.0.9=20=E2=86=92=2067?= =?UTF-8?q?.0.10=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cg/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 79429b895b..6d94999c77 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 67.0.9 +current_version = 67.0.10 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index f477f5d8b4..e1ddfe0181 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "67.0.9" +__version__ = "67.0.10" diff --git a/pyproject.toml b/pyproject.toml index e662a5a5f3..285b65f807 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "cg" -version = "67.0.9" +version = "67.0.10" description = "Clinical Genomics command center" readme = {file = "README.md", content-type = "text/markdown"} homepage = "https://github.com/Clinical-Genomics/cg" From 49f9230e4a1f38355c00cf123c7d58a65cb78aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20Ohlsson=20=C3=85ngnell?= <40887124+islean@users.noreply.github.com> Date: Wed, 29 Jan 2025 13:24:11 +0100 Subject: [PATCH 03/20] Fix broken validation (#4168) (patch) ### Fixed - Region code only set by validator if region is set - Original lab address only set by validator if original lab is set. --- .../orders/validation/workflows/mutant/models/sample.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cg/services/orders/validation/workflows/mutant/models/sample.py b/cg/services/orders/validation/workflows/mutant/models/sample.py index 2b0a972feb..1e3ef5bdf3 100644 --- a/cg/services/orders/validation/workflows/mutant/models/sample.py +++ b/cg/services/orders/validation/workflows/mutant/models/sample.py @@ -43,7 +43,7 @@ class MutantSample(Sample): def set_original_lab_address(cls, data: any) -> any: if isinstance(data, dict): is_set = bool(data.get("original_lab_address")) - if not is_set: + if not is_set and data.get("original_lab"): data["original_lab_address"] = ORIGINAL_LAB_ADDRESSES[data["original_lab"]] return data @@ -52,7 +52,7 @@ def set_original_lab_address(cls, data: any) -> any: def set_region_code(cls, data: any) -> any: if isinstance(data, dict): is_set = bool(data.get("region_code")) - if not is_set: + if not is_set and data.get("region"): data["region_code"] = REGION_CODES[data["region"]] return data From a4f601650bcc004c30e8468cede489cacc9cce25 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Wed, 29 Jan 2025 12:24:55 +0000 Subject: [PATCH 04/20] =?UTF-8?q?Bump=20version:=2067.0.10=20=E2=86=92=206?= =?UTF-8?q?7.0.11=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cg/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6d94999c77..081933e985 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 67.0.10 +current_version = 67.0.11 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index e1ddfe0181..089bf7d26c 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "67.0.10" +__version__ = "67.0.11" diff --git a/pyproject.toml b/pyproject.toml index 285b65f807..9bd5e119de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "cg" -version = "67.0.10" +version = "67.0.11" description = "Clinical Genomics command center" readme = {file = "README.md", content-type = "text/markdown"} homepage = "https://github.com/Clinical-Genomics/cg" From a536099f2eb3f9f85e8c87a45579190dac5e672e Mon Sep 17 00:00:00 2001 From: Peter Pruisscher <57712924+peterpru@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:18:11 +0100 Subject: [PATCH 05/20] Add Nallo workflow metrics-deliver (#4142) ### Added - cg workflow nallo metrics-deliver - nallo to config-case pytests --- cg/cli/workflow/nallo/base.py | 3 +- cg/constants/nf_analysis.py | 4 + cg/meta/workflow/nallo.py | 5 + cg/models/nallo/nallo.py | 6 + .../nf_analysis/test_cli_config_case.py | 6 +- .../nf_analysis/test_cli_metrics_deliver.py | 24 ++- tests/cli/workflow/test_cli_workflow.py | 1 + tests/conftest.py | 168 ++++++++++++++++-- .../fixtures/analysis/nallo/multiqc_data.json | 20 +++ ...allo_fixture_for_metrics_deliverables.yaml | 81 +++++++++ tests/meta/workflow/test_nf_analysis.py | 8 +- 11 files changed, 303 insertions(+), 23 deletions(-) create mode 100644 tests/fixtures/analysis/nallo/multiqc_data.json create mode 100644 tests/fixtures/analysis/nallo/nallo_fixture_for_metrics_deliverables.yaml diff --git a/cg/cli/workflow/nallo/base.py b/cg/cli/workflow/nallo/base.py index 4c724e8561..fb1cad2a76 100644 --- a/cg/cli/workflow/nallo/base.py +++ b/cg/cli/workflow/nallo/base.py @@ -6,7 +6,7 @@ from cg.cli.utils import CLICK_CONTEXT_SETTINGS -from cg.cli.workflow.nf_analysis import config_case, run, start +from cg.cli.workflow.nf_analysis import config_case, run, start, metrics_deliver from cg.constants.constants import MetaApis from cg.meta.workflow.analysis import AnalysisAPI @@ -26,3 +26,4 @@ def nallo(context: click.Context) -> None: nallo.add_command(config_case) nallo.add_command(run) nallo.add_command(start) +nallo.add_command(metrics_deliver) diff --git a/cg/constants/nf_analysis.py b/cg/constants/nf_analysis.py index 87e7cd72b9..377e1fbb70 100644 --- a/cg/constants/nf_analysis.py +++ b/cg/constants/nf_analysis.py @@ -15,6 +15,10 @@ class NfTowerStatus(StrEnum): UNKNOWN: str = "UNKNOWN" +NALLO_METRIC_CONDITIONS: dict[str, dict[str, Any]] = { + "median_coverage": {"norm": "gt", "threshold": 25}, +} + RAREDISEASE_PREDICTED_SEX_METRIC = "predicted_sex_sex_check" RAREDISEASE_METRIC_CONDITIONS: dict[str, dict[str, Any]] = { diff --git a/cg/meta/workflow/nallo.py b/cg/meta/workflow/nallo.py index f53b431b04..12cd944cec 100644 --- a/cg/meta/workflow/nallo.py +++ b/cg/meta/workflow/nallo.py @@ -1,7 +1,9 @@ """Module for Nallo Analysis API.""" import logging + from cg.constants import Workflow +from cg.constants.nf_analysis import NALLO_METRIC_CONDITIONS from cg.constants.subject import PlinkPhenotypeStatus, PlinkSex from cg.meta.workflow.nf_analysis import NfAnalysisAPI from cg.models.cg_config import CGConfig @@ -91,3 +93,6 @@ def get_built_workflow_parameters(self, case_id: str) -> NalloParameters: input=self.get_sample_sheet_path(case_id=case_id), outdir=outdir, ) + + def get_workflow_metrics(self, metric_id: str) -> dict: + return NALLO_METRIC_CONDITIONS diff --git a/cg/models/nallo/nallo.py b/cg/models/nallo/nallo.py index c5463d5fe1..46f164ae4a 100644 --- a/cg/models/nallo/nallo.py +++ b/cg/models/nallo/nallo.py @@ -7,6 +7,12 @@ from cg.models.nf_analysis import WorkflowParameters +class NalloQCMetrics(BaseModel): + """Nallo QC metrics""" + + median_coverage: float | None + + class NalloSampleSheetEntry(BaseModel): """Nallo sample model is used when building the sample sheet.""" diff --git a/tests/cli/workflow/nf_analysis/test_cli_config_case.py b/tests/cli/workflow/nf_analysis/test_cli_config_case.py index f3c5dcc834..ff73087a51 100644 --- a/tests/cli/workflow/nf_analysis/test_cli_config_case.py +++ b/tests/cli/workflow/nf_analysis/test_cli_config_case.py @@ -44,7 +44,7 @@ def test_config_case_without_options( @pytest.mark.parametrize( "workflow", - NEXTFLOW_WORKFLOWS, + NEXTFLOW_WORKFLOWS + [Workflow.NALLO], ) def test_config_with_missing_case( cli_runner: CliRunner, @@ -74,7 +74,7 @@ def test_config_with_missing_case( @pytest.mark.parametrize( "workflow", - NEXTFLOW_WORKFLOWS, + NEXTFLOW_WORKFLOWS + [Workflow.NALLO], ) def test_config_case_without_samples( cli_runner: CliRunner, @@ -185,7 +185,7 @@ def test_config_case_default_parameters( @pytest.mark.parametrize( "workflow", - NEXTFLOW_WORKFLOWS, + NEXTFLOW_WORKFLOWS + [Workflow.NALLO], ) def test_config_case_dry_run( cli_runner: CliRunner, diff --git a/tests/cli/workflow/nf_analysis/test_cli_metrics_deliver.py b/tests/cli/workflow/nf_analysis/test_cli_metrics_deliver.py index 08c617f7b0..b105372d38 100644 --- a/tests/cli/workflow/nf_analysis/test_cli_metrics_deliver.py +++ b/tests/cli/workflow/nf_analysis/test_cli_metrics_deliver.py @@ -18,7 +18,13 @@ @pytest.mark.parametrize( "workflow", - [Workflow.RAREDISEASE, Workflow.RNAFUSION, Workflow.TAXPROFILER, Workflow.TOMTE], + [ + Workflow.RAREDISEASE, + Workflow.RNAFUSION, + Workflow.TAXPROFILER, + Workflow.TOMTE, + Workflow.NALLO, + ], ) def test_metrics_deliver_without_options( cli_runner: CliRunner, workflow: Workflow, request: FixtureRequest @@ -38,7 +44,13 @@ def test_metrics_deliver_without_options( @pytest.mark.parametrize( "workflow", - [Workflow.RAREDISEASE, Workflow.RNAFUSION, Workflow.TAXPROFILER, Workflow.TOMTE], + [ + Workflow.RAREDISEASE, + Workflow.RNAFUSION, + Workflow.TAXPROFILER, + Workflow.TOMTE, + Workflow.NALLO, + ], ) def test_metrics_deliver_with_missing_case( cli_runner: CliRunner, @@ -69,7 +81,13 @@ def test_metrics_deliver_with_missing_case( @pytest.mark.parametrize( "workflow", - [Workflow.RAREDISEASE, Workflow.RNAFUSION, Workflow.TAXPROFILER, Workflow.TOMTE], + [ + Workflow.RAREDISEASE, + Workflow.RNAFUSION, + Workflow.TAXPROFILER, + Workflow.TOMTE, + Workflow.NALLO, + ], ) def test_metrics_deliver_case( cli_runner: CliRunner, diff --git a/tests/cli/workflow/test_cli_workflow.py b/tests/cli/workflow/test_cli_workflow.py index 80bb612b75..91ff6c18c3 100644 --- a/tests/cli/workflow/test_cli_workflow.py +++ b/tests/cli/workflow/test_cli_workflow.py @@ -21,6 +21,7 @@ def test_no_options(cli_runner: CliRunner, base_context: CGConfig): assert "microsalt" in result.output assert "mip-dna" in result.output assert "mip-rna" in result.output + assert "nallo" in result.output assert "raredisease" in result.output assert "rnafusion" in result.output assert "taxprofiler" in result.output diff --git a/tests/conftest.py b/tests/conftest.py index 34bcdbe368..4310773e93 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -868,6 +868,12 @@ def mip_dna_analysis_dir(mip_analysis_dir: Path) -> Path: return Path(mip_analysis_dir, "dna") +@pytest.fixture +def nallo_analysis_dir(analysis_dir: Path) -> Path: + """Return the path to the directory with nallo analysis files.""" + return Path(analysis_dir, "nallo") + + @pytest.fixture def nf_analysis_analysis_dir(fixtures_dir: Path) -> Path: """Return the path to the directory with nf-analysis files.""" @@ -2115,7 +2121,7 @@ def context_config( "workflow_bin_path": Path("workflow", "path").as_posix(), "profile": "myprofile", "references": Path("path", "to", "references").as_posix(), - "revision": "dev", + "revision": "2.2.0", "root": str(nallo_dir), "slurm": { "account": "development", @@ -2523,7 +2529,7 @@ def bam_unmapped_read_paths(housekeeper_dir: Path) -> Path: """Path to existing bam read file.""" bam_unmapped_read_path = Path( housekeeper_dir, "m00000_000000_000000_s4.hifi_reads.bc2021" - ).with_suffix(f"{AlignmentFileTag.BAM}") + ).with_suffix(f".{AlignmentFileTag.BAM}") with open(bam_unmapped_read_path, "wb") as wh: wh.write( b"1f 8b 08 04 00 00 00 00 00 ff 06 00 42 43 02 00 1b 00 03 00 00 00 00 00 00 00 00 00" @@ -2541,7 +2547,7 @@ def sequencing_platform() -> str: @pytest.fixture(scope="session") def nallo_case_id() -> str: """Returns a nallo case id.""" - return "nallo_case_two_samples" + return "nallo_case_enough_reads" @pytest.fixture(scope="function") @@ -2550,62 +2556,88 @@ def nallo_context( helpers: StoreHelpers, nf_analysis_housekeeper: HousekeeperAPI, trailblazer_api: MockTB, - hermes_api: HermesApi, - cg_dir: Path, nallo_case_id: str, sample_id: str, + father_sample_id: str, sample_name: str, another_sample_name: str, - father_sample_id: str, no_sample_case_id: str, + total_sequenced_reads_pass: int, wgs_long_read_application_tag: str, + case_id_not_enough_reads: str, + sample_id_not_enough_reads: str, + total_sequenced_reads_not_pass: int, ) -> CGConfig: - """Context to use in CLI.""" + """context to use in cli""" cg_context.housekeeper_api_ = nf_analysis_housekeeper cg_context.trailblazer_api_ = trailblazer_api cg_context.meta_apis["analysis_api"] = NalloAnalysisAPI(config=cg_context) status_db: Store = cg_context.status_db + # NB: the order in which the cases are added matters for the tests of store_available + # Create ERROR case with NO SAMPLES helpers.add_case(status_db, internal_id=no_sample_case_id, name=no_sample_case_id) - # Create textbook case with two samples - nallo_case_two_samples: Case = helpers.add_case( + # Create textbook case with enough reads + case_enough_reads: Case = helpers.add_case( store=status_db, internal_id=nallo_case_id, name=nallo_case_id, data_analysis=Workflow.NALLO, ) - nallo_sample_one: Sample = helpers.add_sample( + sample_enough_reads: Sample = helpers.add_sample( status_db, internal_id=sample_id, name=sample_name, last_sequenced_at=datetime.now(), + reads=total_sequenced_reads_pass, application_tag=wgs_long_read_application_tag, reference_genome=GenomeVersion.HG38, ) - another_nallo_sample: Sample = helpers.add_sample( + another_sample_enough_reads: Sample = helpers.add_sample( status_db, internal_id=father_sample_id, name=another_sample_name, last_sequenced_at=datetime.now(), + reads=total_sequenced_reads_pass, application_tag=wgs_long_read_application_tag, reference_genome=GenomeVersion.HG38, ) helpers.add_relationship( status_db, - case=nallo_case_two_samples, - sample=nallo_sample_one, + case=case_enough_reads, + sample=sample_enough_reads, ) helpers.add_relationship( status_db, - case=nallo_case_two_samples, - sample=another_nallo_sample, + case=case_enough_reads, + sample=another_sample_enough_reads, ) + + # Create case without enough reads + case_not_enough_reads: Case = helpers.add_case( + store=status_db, + internal_id=case_id_not_enough_reads, + name=case_id_not_enough_reads, + data_analysis=Workflow.NALLO, + ) + + sample_not_enough_reads: Sample = helpers.add_sample( + status_db, + internal_id=sample_id_not_enough_reads, + last_sequenced_at=datetime.now(), + reads=total_sequenced_reads_not_pass, + application_tag=wgs_long_read_application_tag, + reference_genome=GenomeVersion.HG38, + ) + + helpers.add_relationship(status_db, case=case_not_enough_reads, sample=sample_not_enough_reads) + return cg_context @@ -2625,6 +2657,112 @@ def nallo_config(nallo_dir: Path, nallo_case_id: str) -> None: ).touch(exist_ok=True) +@pytest.fixture(scope="function") +def nallo_deliverable_data(nallo_dir: Path, nallo_case_id: str, sample_id: str) -> dict: + return { + "files": [ + { + "path": f"{nallo_dir}/{nallo_case_id}/multiqc/multiqc_report.html", + "path_index": "", + "step": "report", + "tag": ["multiqc-html"], + "id": nallo_case_id, + "format": "html", + "mandatory": True, + }, + ] + } + + +@pytest.fixture(scope="function") +def nallo_metrics_deliverables(nallo_analysis_dir: Path) -> list[dict]: + """Returns the content of a mock metrics deliverables file.""" + return read_yaml( + file_path=Path(nallo_analysis_dir, "nallo_fixture_for_metrics_deliverables.yaml") + ) + + +@pytest.fixture(scope="function") +def nallo_metrics_deliverables_path(nallo_dir: Path, nallo_case_id: str) -> Path: + """Path to deliverables file.""" + return Path(nallo_dir, nallo_case_id, f"{nallo_case_id}_metrics_deliverables").with_suffix( + FileExtensions.YAML + ) + + +@pytest.fixture(scope="function") +def nallo_mock_analysis_finish( + nallo_dir: Path, + nallo_case_id: str, + nallo_multiqc_json_metrics: dict, + tower_id: int, +) -> None: + """Create analysis finish file for testing.""" + Path.mkdir(Path(nallo_dir, nallo_case_id, "pipeline_info"), parents=True, exist_ok=True) + Path(nallo_dir, nallo_case_id, "pipeline_info", software_version_file).touch(exist_ok=True) + Path(nallo_dir, nallo_case_id, f"{nallo_case_id}_samplesheet.csv").touch(exist_ok=True) + Path.mkdir( + Path(nallo_dir, nallo_case_id, "multiqc", "multiqc_data"), + parents=True, + exist_ok=True, + ) + write_json( + content=nallo_multiqc_json_metrics, + file_path=Path( + nallo_dir, + nallo_case_id, + "multiqc", + "multiqc_data", + "multiqc_data", + ).with_suffix(FileExtensions.JSON), + ) + write_yaml( + content={nallo_case_id: [tower_id]}, + file_path=Path( + nallo_dir, + nallo_case_id, + "tower_ids", + ).with_suffix(FileExtensions.YAML), + ) + + +@pytest.fixture(scope="function") +def nallo_mock_deliverable_dir( + nallo_dir: Path, nallo_deliverable_data: dict, nallo_case_id: str +) -> Path: + """Create nallo deliverable file with dummy data and files to deliver.""" + Path.mkdir( + Path(nallo_dir, nallo_case_id), + parents=True, + exist_ok=True, + ) + Path.mkdir( + Path(nallo_dir, nallo_case_id, "multiqc"), + parents=True, + exist_ok=True, + ) + for report_entry in nallo_deliverable_data["files"]: + Path(report_entry["path"]).touch(exist_ok=True) + WriteFile.write_file_from_content( + content=nallo_deliverable_data, + file_format=FileFormat.JSON, + file_path=Path(nallo_dir, nallo_case_id, nallo_case_id + deliverables_yaml), + ) + return nallo_dir + + +@pytest.fixture +def nallo_multiqc_json_metrics_path(nallo_analysis_dir: Path) -> Path: + """Return Multiqc JSON file path for nallo.""" + return Path(nallo_analysis_dir, multiqc_json_file) + + +@pytest.fixture(scope="function") +def nallo_multiqc_json_metrics(nallo_analysis_dir) -> dict: + """Returns the content of a mock Multiqc JSON file.""" + return read_json(file_path=Path(nallo_analysis_dir, multiqc_json_file)) + + @pytest.fixture(scope="function") def nallo_nexflow_config_file_path(nallo_dir, nallo_case_id) -> Path: """Path to config file.""" diff --git a/tests/fixtures/analysis/nallo/multiqc_data.json b/tests/fixtures/analysis/nallo/multiqc_data.json new file mode 100644 index 0000000000..ef0c3b817e --- /dev/null +++ b/tests/fixtures/analysis/nallo/multiqc_data.json @@ -0,0 +1,20 @@ +{ + "report_data_sources": {}, + "report_general_stats_data": [ + { + "ADM1": { + "mean_coverage": 32.18, + "min_coverage": 0.0, + "max_coverage": 63906.0, + "coverage_bases": 99366056494, + "length": 3088286377, + "1_x_pc": 94.0, + "5_x_pc": 94.0, + "10_x_pc": 93.0, + "30_x_pc": 65.0, + "50_x_pc": 2.0, + "median_coverage": 33 + } + } + ] +} diff --git a/tests/fixtures/analysis/nallo/nallo_fixture_for_metrics_deliverables.yaml b/tests/fixtures/analysis/nallo/nallo_fixture_for_metrics_deliverables.yaml new file mode 100644 index 0000000000..44e0066941 --- /dev/null +++ b/tests/fixtures/analysis/nallo/nallo_fixture_for_metrics_deliverables.yaml @@ -0,0 +1,81 @@ +--- +metrics: +- condition: null + header: null + id: ADM1 + input: multiqc_data.json + name: mean_coverage + step: multiqc + value: 32.18 +- condition: null + header: null + id: ADM1 + input: multiqc_data.json + name: min_coverage + step: multiqc + value: 0.0 +- condition: null + header: null + id: ADM1 + input: multiqc_data.json + name: max_coverage + step: multiqc + value: 63906.0 +- condition: null + header: null + id: ADM1 + input: multiqc_data.json + name: coverage_bases + step: multiqc + value: 99366056494 +- condition: null + header: null + id: ADM1 + input: multiqc_data.json + name: length + step: multiqc + value: 3088286377 +- condition: null + header: null + id: ADM1 + input: multiqc_data.json + name: 1_x_pc + step: multiqc + value: 94.0 +- condition: null + header: null + id: ADM1 + input: multiqc_data.json + name: 5_x_pc + step: multiqc + value: 94.0 +- condition: null + header: null + id: ADM1 + input: multiqc_data.json + name: 10_x_pc + step: multiqc + value: 93.0 +- condition: null + header: null + id: ADM1 + input: multiqc_data.json + name: 30_x_pc + step: multiqc + value: 65.0 +- condition: null + header: null + id: ADM1 + input: multiqc_data.json + name: 50_x_pc + step: multiqc + value: 2.0 +- condition: + norm: gt + threshold: 25.0 + header: null + id: ADM1 + input: multiqc_data.json + name: median_coverage + step: multiqc + value: 33 \ No newline at end of file diff --git a/tests/meta/workflow/test_nf_analysis.py b/tests/meta/workflow/test_nf_analysis.py index e6e58642ae..095638efce 100644 --- a/tests/meta/workflow/test_nf_analysis.py +++ b/tests/meta/workflow/test_nf_analysis.py @@ -15,7 +15,13 @@ @pytest.mark.parametrize( "workflow", - [Workflow.RAREDISEASE, Workflow.RNAFUSION, Workflow.TAXPROFILER, Workflow.TOMTE], + [ + Workflow.RAREDISEASE, + Workflow.RNAFUSION, + Workflow.TAXPROFILER, + Workflow.TOMTE, + Workflow.NALLO, + ], ) def test_create_metrics_deliverables_content( workflow: Workflow, From 8d71afc0fc97e840c381790056e2c8e89826ee1d Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Wed, 29 Jan 2025 13:18:36 +0000 Subject: [PATCH 06/20] =?UTF-8?q?Bump=20version:=2067.0.11=20=E2=86=92=206?= =?UTF-8?q?7.0.12=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cg/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 081933e985..85e979129f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 67.0.11 +current_version = 67.0.12 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index 089bf7d26c..dcdff22244 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "67.0.11" +__version__ = "67.0.12" diff --git a/pyproject.toml b/pyproject.toml index 9bd5e119de..e476659235 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "cg" -version = "67.0.11" +version = "67.0.12" description = "Clinical Genomics command center" readme = {file = "README.md", content-type = "text/markdown"} homepage = "https://github.com/Clinical-Genomics/cg" From 90c6f6f59b37c5f3e2fde549b01c2dd47a59aa36 Mon Sep 17 00:00:00 2001 From: Annick Renevey <47788523+rannick@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:56:31 +0100 Subject: [PATCH 07/20] Generate deliverables.yaml file for nallo (#4156) | patch ### Added - List of nallo file paths for hermes - Generate deliverables.yaml file for nallo --- cg/cli/workflow/nallo/base.py | 5 +- cg/meta/workflow/nallo.py | 10 +- cg/resources/__init__.py | 15 +- cg/resources/nallo_bundle_filenames.yaml | 313 ++++++++++++++++++ .../raredisease_bundle_filenames.yaml | 2 +- 5 files changed, 332 insertions(+), 13 deletions(-) create mode 100644 cg/resources/nallo_bundle_filenames.yaml diff --git a/cg/cli/workflow/nallo/base.py b/cg/cli/workflow/nallo/base.py index fb1cad2a76..c7d9a20bdb 100644 --- a/cg/cli/workflow/nallo/base.py +++ b/cg/cli/workflow/nallo/base.py @@ -5,9 +5,7 @@ import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS - -from cg.cli.workflow.nf_analysis import config_case, run, start, metrics_deliver - +from cg.cli.workflow.nf_analysis import config_case, metrics_deliver, report_deliver, run, start from cg.constants.constants import MetaApis from cg.meta.workflow.analysis import AnalysisAPI from cg.meta.workflow.nallo import NalloAnalysisAPI @@ -24,6 +22,7 @@ def nallo(context: click.Context) -> None: nallo.add_command(config_case) +nallo.add_command(report_deliver) nallo.add_command(run) nallo.add_command(start) nallo.add_command(metrics_deliver) diff --git a/cg/meta/workflow/nallo.py b/cg/meta/workflow/nallo.py index 12cd944cec..87f7ee08a9 100644 --- a/cg/meta/workflow/nallo.py +++ b/cg/meta/workflow/nallo.py @@ -1,15 +1,16 @@ """Module for Nallo Analysis API.""" import logging +from pathlib import Path from cg.constants import Workflow from cg.constants.nf_analysis import NALLO_METRIC_CONDITIONS from cg.constants.subject import PlinkPhenotypeStatus, PlinkSex from cg.meta.workflow.nf_analysis import NfAnalysisAPI from cg.models.cg_config import CGConfig -from cg.models.nallo.nallo import NalloSampleSheetHeaders, NalloSampleSheetEntry, NalloParameters +from cg.models.nallo.nallo import NalloParameters, NalloSampleSheetEntry, NalloSampleSheetHeaders +from cg.resources import NALLO_BUNDLE_FILENAMES_PATH from cg.store.models import CaseSample -from pathlib import Path LOG = logging.getLogger(__name__) @@ -94,5 +95,10 @@ def get_built_workflow_parameters(self, case_id: str) -> NalloParameters: outdir=outdir, ) + @staticmethod + def get_bundle_filenames_path() -> Path: + """Return Nallo bundle filenames path.""" + return NALLO_BUNDLE_FILENAMES_PATH + def get_workflow_metrics(self, metric_id: str) -> dict: return NALLO_METRIC_CONDITIONS diff --git a/cg/resources/__init__.py b/cg/resources/__init__.py index 36f2723f09..46c3de3d9a 100644 --- a/cg/resources/__init__.py +++ b/cg/resources/__init__.py @@ -5,26 +5,27 @@ project_root_dir: Path = get_project_root_dir() +NALLO_BUNDLE_FILENAMES: str = ( + Path("resources", "nallo_bundle_filenames").with_suffix(FileExtensions.YAML).as_posix() +) +NALLO_BUNDLE_FILENAMES_PATH = Path(project_root_dir, NALLO_BUNDLE_FILENAMES) + RAREDISEASE_BUNDLE_FILENAMES: str = ( Path("resources", "raredisease_bundle_filenames").with_suffix(FileExtensions.YAML).as_posix() ) +RAREDISEASE_BUNDLE_FILENAMES_PATH = Path(project_root_dir, RAREDISEASE_BUNDLE_FILENAMES) RNAFUSION_BUNDLE_FILENAMES: str = ( Path("resources", "rnafusion_bundle_filenames").with_suffix(FileExtensions.YAML).as_posix() ) +RNAFUSION_BUNDLE_FILENAMES_PATH = Path(project_root_dir, RNAFUSION_BUNDLE_FILENAMES) TAXPROFILER_BUNDLE_FILENAMES: str = ( Path("resources", "taxprofiler_bundle_filenames").with_suffix(FileExtensions.YAML).as_posix() ) +TAXPROFILER_BUNDLE_FILENAMES_PATH = Path(project_root_dir, TAXPROFILER_BUNDLE_FILENAMES) TOMTE_BUNDLE_FILENAMES: str = ( Path("resources", "tomte_bundle_filenames").with_suffix(FileExtensions.YAML).as_posix() ) - -RAREDISEASE_BUNDLE_FILENAMES_PATH = Path(project_root_dir, RAREDISEASE_BUNDLE_FILENAMES) - -RNAFUSION_BUNDLE_FILENAMES_PATH = Path(project_root_dir, RNAFUSION_BUNDLE_FILENAMES) - -TAXPROFILER_BUNDLE_FILENAMES_PATH = Path(project_root_dir, TAXPROFILER_BUNDLE_FILENAMES) - TOMTE_BUNDLE_FILENAMES_PATH = Path(project_root_dir, TOMTE_BUNDLE_FILENAMES) diff --git a/cg/resources/nallo_bundle_filenames.yaml b/cg/resources/nallo_bundle_filenames.yaml new file mode 100644 index 0000000000..a1fc821dcc --- /dev/null +++ b/cg/resources/nallo_bundle_filenames.yaml @@ -0,0 +1,313 @@ + +- format: bam + id: SAMPLEID + path: PATHTOCASE/CASEID/aligned_reads/SAMPLENAME/SAMPLENAME_haplotagged.bam + path_index: ~ + step: alignment + tag: alignment_haplotags +- format: bam + id: SAMPLEID + path: PATHTOCASE/CASEID/aligned_reads/SAMPLENAME/SAMPLENAME_haplotagged.bam.bai + path_index: ~ + step: alignment + tag: alignment_haplotags_index +- format: meta + id: SAMPLEID + path: PATHTOCASE/CASEID/assembly_haplotypes/gfastats/SAMPLENAME/SAMPLENAME.asm.bp.hap1.p_ctg.assembly_summary + path_index: ~ + step: assembly + tag: summary_hap1 +- format: meta + id: SAMPLEID + path: PATHTOCASE/CASEID/assembly_haplotypes/gfastats/SAMPLENAME/SAMPLENAME.asm.bp.hap2.p_ctg.assembly_summary + path_index: ~ + step: assembly + tag: summary_hap1 +- format: bam + id: SAMPLEID + path: PATHTOCASE/CASEID/assembly_variant_calling/dipcall/SAMPLENAME/SAMPLENAME.hap1.bam + path_index: ~ + step: assembly + tag: assembly_hap1_mapped +- format: bam + id: SAMPLEID + path: PATHTOCASE/CASEID/assembly_variant_calling/dipcall/SAMPLENAME/SAMPLENAME.hap1.bam.bai + path_index: ~ + step: assembly + tag: assembly_hap1_mapped_index +- format: bam + id: SAMPLEID + path: PATHTOCASE/CASEID/assembly_variant_calling/dipcall/SAMPLENAME/SAMPLENAME.hap2.bam + path_index: ~ + step: assembly + tag: assembly_hap2_mapped +- format: bam + id: SAMPLEID + path: PATHTOCASE/CASEID/assembly_variant_calling/dipcall/SAMPLENAME/SAMPLENAME.hap2.bam.bai + path_index: ~ + step: assembly + tag: assembly_hap2_mapped_index +- format: bed + id: SAMPLEID + path: PATHTOCASE/CASEID/methylation/modkit/pileup/SAMPLENAME/SAMPLENAME_modkit_pileup_1.bed.gz + path_index: ~ + step: summary_counts + tag: hap1 +- format: bed + id: SAMPLEID + path: PATHTOCASE/CASEID/methylation/modkit/pileup/SAMPLENAME/SAMPLENAME_modkit_pileup_1.bed.gz/tbi + path_index: ~ + step: summary_counts + tag: hap1_index +- format: bed + id: SAMPLEID + path: PATHTOCASE/CASEID/methylation/modkit/pileup/SAMPLENAME/SAMPLENAME_modkit_pileup_2.bed.gz + path_index: ~ + step: summary_counts + tag: hap2 +- format: bed + id: SAMPLEID + path: PATHTOCASE/CASEID/methylation/modkit/pileup/SAMPLENAME/SAMPLENAME_modkit_pileup_2.bed.gz/tbi + path_index: ~ + step: summary_counts + tag: hap2_index +- format: bed + id: SAMPLEID + path: PATHTOCASE/CASEID/methylation/modkit/pileup/SAMPLENAME/SAMPLENAME_modkit_pileup_2.bed.gz + path_index: ~ + step: summary_counts + tag: ungrouped +- format: bed + id: SAMPLEID + path: PATHTOCASE/CASEID/methylation/modkit/pileup/SAMPLENAME/SAMPLENAME_modkit_pileup_ungrouped.bed.gz/tbi + path_index: ~ + step: summary_counts + tag: ungrouped_index +- format: d4 + id: SAMPLEID + path: PATHTOCASE/qc_bam/SAMPLEID_mosdepth.per-base.d4 + path_index: ~ + step: qc_bam + tag: mosdepth_d4 +- format: meta + id: CASEID + path: PATHTOCASE/pedigree/CASEID.ped + path_index: ~ + step: pedigree + tag: pedigree_fam +- format: meta + id: CASEID + path: PATHTOCASE/CASEID/qc/somalier/relate/CASEID/CASEID.html + path_index: ~ + step: somalier + tag: relate_html +- format: tsv + id: CASEID + path: PATHTOCASE/CASEID/qc/somalier/relate/CASEID/CASEID.pairs.tsv + path_index: ~ + step: somalier + tag: relate_pairs +- format: tsv + id: CASEID + path: PATHTOCASE/CASEID/qc/somalier/relate/CASEID/CASEID.samples.tsv + path_index: ~ + step: somalier + tag: relate_samples +- format: meta + id: SAMPLEID + path: PATHTOCASE/CASEID/qc/deepvariant_vcfstatsreport/SAMPLENAME/SAMPLENAME.visual_report.html + path_index: ~ + step: deepvariant + tag: report +- format: bam + id: SAMPLEID + path: PATHTOCASE/CASEID/paraphase/SAMPLENAME/SAMPLENAME.paraphase.bam + path_index: ~ + step: paraphase + tag: paraphase +- format: bam + id: SAMPLEID + path: PATHTOCASE/CASEID/paraphase/SAMPLENAME/SAMPLENAME.paraphase.bam.bai + path_index: ~ + step: paraphase + tag: paraphase_index +- format: json + id: SAMPLEID + path: PATHTOCASE/CASEID/paraphase/SAMPLENAME/SAMPLENAME.paraphase.json + path_index: ~ + step: paraphase + tag: json +- format: vcf + id: SAMPLEID + path: PATHTOCASE/CASEID/paraphase/SAMPLENAME/SAMPLENAME_paraphase_vcfs/SAMPLENAME_*.vcf.gz + path_index: ~ + step: paraphase + tag: vcf +- format: vcf + id: SAMPLEID + path: PATHTOCASE/CASEID/paraphase/SAMPLENAME/SAMPLENAME_paraphase_vcfs/SAMPLENAME_*.vcf.gz.tbi + path_index: ~ + step: paraphase + tag: vcf_index +- format: vcf + id: SAMPLEID + path: PATHTOCASE/CASEID/repeats/sample/SAMPLENAME/SAMPLENAME_sorted.vcf.gz + path_index: ~ + step: sorted_repeats + tag: vcf_str +- format: vcf + id: SAMPLEID + path: PATHTOCASE/CASEID/repeats/sample/SAMPLENAME/SAMPLENAME_sorted.vcf.gz.tbi + path_index: ~ + step: sorted_repeats + tag: vcf_str_index +- format: bam + id: SAMPLEID + path: PATHTOCASE/CASEID/repeats/sample/SAMPLENAME/SAMPLENAME_spanning_sorted.bam + path_index: ~ + step: spanning_repeats + tag: bam +- format: bam + id: SAMPLEID + path: PATHTOCASE/CASEID/repeats/sample/SAMPLENAME/SAMPLENAME_spanning_sorted.bam.bai + path_index: ~ + step: spanning_repeats + tag: bam_index +- format: vcf + id: SAMPLEID + path: PATHTOCASE/CASEID/repeats/family/CASEID/CASEID_repeats_annotated.vcf.gz + path_index: ~ + step: repeats_annotated + tag: vcf_str +- format: vcf + id: SAMPLEID + path: PATHTOCASE/CASEID/repeats/family/CASEID/CASEID_repeats_annotated.vcf.gz.tbi + path_index: ~ + step: repeats_annotated + tag: vcf_str_index +- format: vcf + id: CASEID + path: PATHTOCASE/CASEID/snvs/family/CASEID/CASEID_snv_annotated_ranked.vcf.gz + path_index: ~ + step: snv_annotated + tag: vcf_snv_research +- format: vcf + id: CASEID + path: PATHTOCASE/CASEID/snvs/family/CASEID/CASEID_snv_annotated_ranked.vcf.gz.tbi + path_index: ~ + step: snv_annotated + tag: vcf_snv_research_index +- format: vcf + id: CASEID + path: PATHTOCASE/CASEID/snvs/family/CASEID/CASEID_snvs_annotated_ranked_filtered.vcf.gz + path_index: ~ + step: snv_annotated_filtered + tag: vcf_snv_clinical +- format: vcf + id: CASEID + path: PATHTOCASE/CASEID/snvs/family/CASEID/CASEID_snvs_annotated_ranked_filtered.vcf.gz.tbi + path_index: ~ + step: snv_annotated_filtered + tag: vcf_snv_clinical_index +- format: vcf + id: CASEID + path: PATHTOCASE/CASEID/snvs/family/CASEID/CASEID_snvs_annotated_ranked_filtered.vcf.gz + path_index: ~ + step: snv_annotated_filtered + tag: vcf_snv_clinical +- format: vcf + id: CASEID + path: PATHTOCASE/CASEID/snvs/family/CASEID/CASEID_snvs_annotated_ranked_filtered.vcf.gz.tbi + path_index: ~ + step: snv_annotated_filtered + tag: vcf_snv_clinical_index +- format: vcf + id: CASEID + path: PATHTOCASE/CASEID/svs/family/CASEID/CASEID_svs_cnvs_merged_annotated_ranked.vcf.gz + path_index: ~ + step: sv_annotated_ranked + tag: vcf_sv_research +- format: vcf + id: CASEID + path: PATHTOCASE/CASEID/svs/family/CASEID/CASEID_svs_cnvs_merged_annotated_ranked.vcf.gz.tbi + path_index: ~ + step: sv_annotated_ranked + tag: vcf_sv_research_index +- format: vcf + id: CASEID + path: PATHTOCASE/CASEID/svs/family/CASEID/CASEID_svs_cnvs_merged_annotated_ranked_filtered.vcf.gz + path_index: ~ + step: sv_annotated_ranked_filtered + tag: vcf_sv_clinical +- format: vcf + id: CASEID + path: PATHTOCASE/CASEID/svs/family/CASEID/CASEID_svs_cnvs_merged_annotated_ranked_filtered.vcf.gz.tbi + path_index: ~ + step: sv_annotated_ranked_filtered + tag: vcf_sv_clinical_index +- format: meta + id: SAMPLEID + path: PATHTOCASE/CASEID/visualization_tracks/SAMPLENAME/SAMPLENAME_hificnv.copynum.bedgraph + path_index: ~ + step: copy_number + tag: bedgraph +- format: bw + id: SAMPLEID + path: PATHTOCASE/CASEID/visualization_tracks/SAMPLENAME/SAMPLENAME_hificnv.depth.bw + path_index: ~ + step: depth_track + tag: bigwig +- format: bw + id: SAMPLEID + path: PATHTOCASE/CASEID/visualization_tracks/SAMPLENAME/SAMPLENAME_hificnv.maf.bw + path_index: ~ + step: maf_depth_track + tag: bigwig +- format: meta + id: CASEID + path: PATHTOCASE/multiqc/multiqc_data/multiqc_data.json + path_index: ~ + step: multiqc + tag: multiqc-json +- format: meta + id: CASEID + path: PATHTOCASE/multiqc/multiqc_report.html + path_index: ~ + step: multiqc + tag: multiqc-html +- format: csv + id: CASEID + path: PATHTOCASE/CASEID_samplesheet.csv + path_index: ~ + step: samplesheet + tag: samplesheet +- format: yaml + id: CASEID + path: PATHTOCASE/CASEID_params_file.yaml + path_index: ~ + step: nextflow-params + tag: nextflow-params +- format: json + id: CASEID + path: PATHTOCASE/CASEID_nextflow_config.json + path_index: ~ + step: nextflow-config + tag: nextflow-config +- format: json + id: CASEID + path: PATHTOCASE/manifest.json + path_index: ~ + step: manifest + tag: manifest +- format: yml + id: CASE_ID + path: /CASE_ID/pipeline_info/nallo_pipeline_software_mqc_versions.yml + path_index: ~ + step: software-versions + tag: software-versions +- format: yaml + id: CASE_ID + path: /CASE_ID/CASE_ID_metrics_deliverables.yaml + path_index: ~ + step: qc-metrics + tag: qc-metrics diff --git a/cg/resources/raredisease_bundle_filenames.yaml b/cg/resources/raredisease_bundle_filenames.yaml index 21ca8587c4..57f5cc0838 100644 --- a/cg/resources/raredisease_bundle_filenames.yaml +++ b/cg/resources/raredisease_bundle_filenames.yaml @@ -28,7 +28,7 @@ path_index: ~ step: tiddit_coverage tag: tiddit_coverage -- format: d4 +- format: meta id: SAMPLEID path: PATHTOCASE/qc_bam/SAMPLEID_mosdepth.per-base.d4 path_index: ~ From b393613b4f29f1bd77093fa34aa38095a1538582 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Thu, 30 Jan 2025 09:56:59 +0000 Subject: [PATCH 08/20] =?UTF-8?q?Bump=20version:=2067.0.12=20=E2=86=92=206?= =?UTF-8?q?7.0.13=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cg/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 85e979129f..3bc50d74aa 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 67.0.12 +current_version = 67.0.13 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index dcdff22244..07a00976a3 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "67.0.12" +__version__ = "67.0.13" diff --git a/pyproject.toml b/pyproject.toml index e476659235..d7238d800b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "cg" -version = "67.0.12" +version = "67.0.13" description = "Clinical Genomics command center" readme = {file = "README.md", content-type = "text/markdown"} homepage = "https://github.com/Clinical-Genomics/cg" From f1e582deac25ebd73b553e5578112f8d6f45a4c9 Mon Sep 17 00:00:00 2001 From: Peter Pruisscher <57712924+peterpru@users.noreply.github.com> Date: Thu, 30 Jan 2025 12:43:36 +0100 Subject: [PATCH 09/20] Add cg workflow nallo panel (#4155) ### Added - cg workflow nallo panel ### Changed - cg workflow nallo config-case triggers panel generation --- cg/cli/workflow/nallo/base.py | 23 +++++++- cg/constants/scout.py | 1 + cg/meta/workflow/nallo.py | 38 +++++++++++++ cg/models/nallo/nallo.py | 2 + tests/cli/workflow/nallo/__init__.py | 0 .../nallo/test_cli_nallo_compound_commands.py | 55 +++++++++++++++++++ tests/conftest.py | 6 ++ 7 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 tests/cli/workflow/nallo/__init__.py create mode 100644 tests/cli/workflow/nallo/test_cli_nallo_compound_commands.py diff --git a/cg/cli/workflow/nallo/base.py b/cg/cli/workflow/nallo/base.py index c7d9a20bdb..c765948cc0 100644 --- a/cg/cli/workflow/nallo/base.py +++ b/cg/cli/workflow/nallo/base.py @@ -4,11 +4,15 @@ import rich_click as click -from cg.cli.utils import CLICK_CONTEXT_SETTINGS + +from cg.cli.utils import CLICK_CONTEXT_SETTINGS, echo_lines +from cg.cli.workflow.commands import ARGUMENT_CASE_ID +from cg.constants.cli_options import DRY_RUN from cg.cli.workflow.nf_analysis import config_case, metrics_deliver, report_deliver, run, start from cg.constants.constants import MetaApis from cg.meta.workflow.analysis import AnalysisAPI from cg.meta.workflow.nallo import NalloAnalysisAPI +from cg.models.cg_config import CGConfig LOG = logging.getLogger(__name__) @@ -26,3 +30,20 @@ def nallo(context: click.Context) -> None: nallo.add_command(run) nallo.add_command(start) nallo.add_command(metrics_deliver) + + +@nallo.command("panel") +@DRY_RUN +@ARGUMENT_CASE_ID +@click.pass_obj +def panel(context: CGConfig, case_id: str, dry_run: bool) -> None: + """Write aggregated gene panel file exported from Scout.""" + + analysis_api: NalloAnalysisAPI = context.meta_apis["analysis_api"] + analysis_api.status_db.verify_case_exists(case_internal_id=case_id) + + bed_lines: list[str] = analysis_api.get_gene_panel(case_id=case_id) + if dry_run: + echo_lines(lines=bed_lines) + return + analysis_api.write_panel_as_tsv(case_id=case_id, content=bed_lines) diff --git a/cg/constants/scout.py b/cg/constants/scout.py index 5371f4275c..812abe5524 100644 --- a/cg/constants/scout.py +++ b/cg/constants/scout.py @@ -14,6 +14,7 @@ class GenomeBuild(StrEnum): class ScoutExportFileName(StrEnum): MANAGED_VARIANTS: str = f"managed_variants{FileExtensions.VCF}" PANELS: str = f"gene_panels{FileExtensions.BED}" + PANELS_TSV: str = f"gene_panels{FileExtensions.TSV}" class UploadTrack(StrEnum): diff --git a/cg/meta/workflow/nallo.py b/cg/meta/workflow/nallo.py index 87f7ee08a9..5e9f65c695 100644 --- a/cg/meta/workflow/nallo.py +++ b/cg/meta/workflow/nallo.py @@ -4,8 +4,11 @@ from pathlib import Path from cg.constants import Workflow +from cg.constants.constants import GenomeVersion, FileFormat from cg.constants.nf_analysis import NALLO_METRIC_CONDITIONS +from cg.constants.scout import ScoutExportFileName from cg.constants.subject import PlinkPhenotypeStatus, PlinkSex +from cg.io.controller import WriteFile from cg.meta.workflow.nf_analysis import NfAnalysisAPI from cg.models.cg_config import CGConfig from cg.models.nallo.nallo import NalloParameters, NalloSampleSheetEntry, NalloSampleSheetHeaders @@ -93,8 +96,43 @@ def get_built_workflow_parameters(self, case_id: str) -> NalloParameters: return NalloParameters( input=self.get_sample_sheet_path(case_id=case_id), outdir=outdir, + filter_variants_hgnc_ids=f"{outdir}/{ScoutExportFileName.PANELS_TSV}", ) + @property + def is_gene_panel_required(self) -> bool: + """Return True if a gene panel needs to be created using information in StatusDB and exporting it from Scout.""" + return True + + def create_gene_panel(self, case_id: str, dry_run: bool) -> None: + """Create and write an aggregated gene panel file exported from Scout as tsv file.""" + LOG.info("Creating gene panel file") + bed_lines: list[str] = self.get_gene_panel(case_id=case_id, dry_run=dry_run) + if dry_run: + bed_lines: str = "\n".join(bed_lines) + LOG.debug(f"{bed_lines}") + return + self.write_panel_as_tsv(case_id=case_id, content=bed_lines) + + def write_panel_as_tsv(self, case_id: str, content: list[str]) -> None: + """Write the gene panel to case dir.""" + self._write_panel_as_tsv(out_dir=Path(self.root, case_id), content=content) + + @staticmethod + def _write_panel_as_tsv(out_dir: Path, content: list[str]) -> None: + """Write the gene panel to case dir while omitted the commented BED lines.""" + filtered_content = [line for line in content if not line.startswith("##")] + out_dir.mkdir(parents=True, exist_ok=True) + WriteFile.write_file_from_content( + content="\n".join(filtered_content), + file_format=FileFormat.TXT, + file_path=Path(out_dir, ScoutExportFileName.PANELS_TSV), + ) + + def get_genome_build(self, case_id: str) -> GenomeVersion: + """Return reference genome for a Nallo case. Currently fixed for hg38.""" + return GenomeVersion.HG38 + @staticmethod def get_bundle_filenames_path() -> Path: """Return Nallo bundle filenames path.""" diff --git a/cg/models/nallo/nallo.py b/cg/models/nallo/nallo.py index 46f164ae4a..42f7bf32d6 100644 --- a/cg/models/nallo/nallo.py +++ b/cg/models/nallo/nallo.py @@ -67,3 +67,5 @@ def list(cls) -> list[str]: class NalloParameters(WorkflowParameters): """Model for Nallo parameters.""" + + filter_variants_hgnc_ids: str diff --git a/tests/cli/workflow/nallo/__init__.py b/tests/cli/workflow/nallo/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/cli/workflow/nallo/test_cli_nallo_compound_commands.py b/tests/cli/workflow/nallo/test_cli_nallo_compound_commands.py new file mode 100644 index 0000000000..5bc77e9dd3 --- /dev/null +++ b/tests/cli/workflow/nallo/test_cli_nallo_compound_commands.py @@ -0,0 +1,55 @@ +from pathlib import Path + +from click.testing import CliRunner +from cg.cli.workflow.nallo.base import panel +from cg.constants.scout import ScoutExportFileName +from cg.io.txt import read_txt +from cg.meta.workflow.nallo import NalloAnalysisAPI +from cg.models.cg_config import CGConfig + + +def test_panel_dry_run( + nallo_case_id: str, + cli_runner: CliRunner, + nallo_context: CGConfig, + scout_panel_output: str, + mocker, +): + # GIVEN a case + + # GIVEN that, the Scout command writes the panel to stdout + mocker.patch.object(NalloAnalysisAPI, "get_gene_panel", return_value=scout_panel_output) + + result = cli_runner.invoke(panel, [nallo_case_id, "--dry-run"], obj=nallo_context) + # THEN the output should contain the output from Scout + actual_output = "".join(result.stdout.strip().split()) + expected_output = "".join(scout_panel_output.strip().split()) + + assert actual_output == expected_output + + +def test_panel_file_is_written( + nallo_case_id: str, + cli_runner: CliRunner, + nallo_context: CGConfig, + scout_panel_output: str, + mocker, +): + # GIVEN an analysis API + analysis_api: NalloAnalysisAPI = nallo_context.meta_apis["analysis_api"] + + # GIVEN a case + + # GIVEN that, the Scout command writes the panel to stdout + mocker.patch.object(NalloAnalysisAPI, "get_gene_panel", return_value=scout_panel_output) + + cli_runner.invoke(panel, [nallo_case_id], obj=nallo_context) + + panel_file = Path(analysis_api.root, nallo_case_id, ScoutExportFileName.PANELS_TSV) + + # THEN the file should exist + assert panel_file.exists() + + # THEN the file should contain the output from Scout + file_content: str = read_txt(file_path=panel_file, read_to_string=True) + assert "".join(file_content.strip().split()) == "".join(scout_panel_output.strip().split()) diff --git a/tests/conftest.py b/tests/conftest.py index 4310773e93..1959135c91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2674,6 +2674,12 @@ def nallo_deliverable_data(nallo_dir: Path, nallo_case_id: str, sample_id: str) } +@pytest.fixture(scope="function") +def nallo_gene_panel_path(nallo_dir, nallo_case_id) -> Path: + """Path to gene panel file.""" + return Path(nallo_dir, nallo_case_id, "gene_panels").with_suffix(FileExtensions.TSV) + + @pytest.fixture(scope="function") def nallo_metrics_deliverables(nallo_analysis_dir: Path) -> list[dict]: """Returns the content of a mock metrics deliverables file.""" From aabb8f7619afb0774edbd282853a3f83e7f30791 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Thu, 30 Jan 2025 11:44:02 +0000 Subject: [PATCH 10/20] =?UTF-8?q?Bump=20version:=2067.0.13=20=E2=86=92=206?= =?UTF-8?q?7.0.14=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cg/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 3bc50d74aa..c276e65d24 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 67.0.13 +current_version = 67.0.14 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index 07a00976a3..f32e8e32ba 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "67.0.13" +__version__ = "67.0.14" diff --git a/pyproject.toml b/pyproject.toml index d7238d800b..3d192e86a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "cg" -version = "67.0.13" +version = "67.0.14" description = "Clinical Genomics command center" readme = {file = "README.md", content-type = "text/markdown"} homepage = "https://github.com/Clinical-Genomics/cg" From eb104d85e926f687543cbbdeae45aaef2e9199dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20Ohlsson=20=C3=85ngnell?= <40887124+islean@users.noreply.github.com> Date: Thu, 30 Jan 2025 14:35:07 +0100 Subject: [PATCH 11/20] Fix RNA uploads/delivery message (#4145) (patch) ### Changed - The RNA uploads to Scout use the query logic found in the ReadHandler - More robust error raising in the ReadHandler for when there are multiple matching DNA samples or when no subject id is passed. --- cg/meta/upload/scout/uploadscoutapi.py | 99 +++-------- cg/store/api/data_classes.py | 11 ++ cg/store/crud/read.py | 158 ++++++++++++------ .../scout/test_meta_upload_scoutapi_rna.py | 52 +++--- tests/store/crud/conftest.py | 36 +--- tests/store/crud/read/test_read_sample.py | 65 ------- 6 files changed, 164 insertions(+), 257 deletions(-) create mode 100644 cg/store/api/data_classes.py diff --git a/cg/meta/upload/scout/uploadscoutapi.py b/cg/meta/upload/scout/uploadscoutapi.py index f08a75e2f1..399cae9900 100644 --- a/cg/meta/upload/scout/uploadscoutapi.py +++ b/cg/meta/upload/scout/uploadscoutapi.py @@ -4,7 +4,6 @@ from pathlib import Path from housekeeper.store.models import File, Version -from pydantic.dataclasses import dataclass from cg.apps.housekeeper.hk import HousekeeperAPI from cg.apps.lims import LimsAPI @@ -26,22 +25,13 @@ from cg.meta.workflow.analysis import AnalysisAPI from cg.meta.workflow.utils.genome_build_helpers import genome_to_scout_format, get_genome_build from cg.models.scout.scout_load_config import ScoutLoadConfig +from cg.store.api.data_classes import RNADNACollection from cg.store.models import Analysis, Case, Customer, Sample from cg.store.store import Store LOG = logging.getLogger(__name__) -@dataclass -class RNADNACollection: - """Contains the id for an RNA sample, the name of its connected DNA sample, - and a list of connected, uploaded DNA cases.""" - - rna_sample_internal_id: str - dna_sample_name: str - dna_case_ids: list[str] - - class UploadScoutAPI: """Class that handles everything that has to do with uploading to Scout.""" @@ -182,15 +172,6 @@ def get_rna_omics_outrider(self, case_id: str) -> File | None: tags: set[str] = {AnalysisTag.OUTRIDER, case_id, AnalysisTag.CLINICAL} return self.housekeeper.get_file_from_latest_version(bundle_name=case_id, tags=tags) - def get_unique_dna_cases_related_to_rna_case(self, case_id: str) -> set[str]: - """Return a set of unique DNA cases related to an RNA case.""" - case: Case = self.status_db.get_case_by_internal_id(case_id) - rna_dna_collections: list[RNADNACollection] = self.create_rna_dna_collections(case) - unique_dna_cases_related_to_rna_case: set[str] = set() - for rna_dna_collection in rna_dna_collections: - unique_dna_cases_related_to_rna_case.update(rna_dna_collection.dna_case_ids) - return unique_dna_cases_related_to_rna_case - def get_rna_alignment_cram(self, case_id: str, sample_id: str) -> File | None: """Return an RNA alignment CRAM file for a case in Housekeeper.""" tags: set[str] = {AlignmentFileTag.CRAM, sample_id} @@ -206,9 +187,11 @@ def get_rna_alignment_cram(self, case_id: str, sample_id: str) -> File | None: def upload_rna_alignment_file(self, case_id: str, dry_run: bool) -> None: """Upload RNA alignment file to Scout.""" rna_case: Case = self.status_db.get_case_by_internal_id(case_id) - rna_dna_collections: list[RNADNACollection] = self.create_rna_dna_collections(rna_case) + rna_dna_collections: list[RNADNACollection] = ( + self.status_db.get_related_dna_cases_with_samples(rna_case) + ) for rna_dna_collection in rna_dna_collections: - rna_sample_internal_id: str = rna_dna_collection.rna_sample_internal_id + rna_sample_internal_id: str = rna_dna_collection.rna_sample_id dna_sample_name: str = rna_dna_collection.dna_sample_name rna_alignment_cram: File | None = self.get_rna_alignment_cram( case_id=case_id, sample_id=rna_sample_internal_id @@ -346,9 +329,11 @@ def upload_rna_coverage_bigwig_to_scout(self, case_id: str, dry_run: bool) -> No status_db: Store = self.status_db rna_case = status_db.get_case_by_internal_id(case_id) - rna_dna_collections: list[RNADNACollection] = self.create_rna_dna_collections(rna_case) + rna_dna_collections: list[RNADNACollection] = ( + self.status_db.get_related_dna_cases_with_samples(rna_case) + ) for rna_dna_collection in rna_dna_collections: - rna_sample_internal_id: str = rna_dna_collection.rna_sample_internal_id + rna_sample_internal_id: str = rna_dna_collection.rna_sample_id dna_sample_name: str = rna_dna_collection.dna_sample_name rna_coverage_bigwig: File | None = self.get_rna_coverage_bigwig( case_id=case_id, sample_id=rna_sample_internal_id @@ -382,7 +367,7 @@ def upload_omics_sample_id_to_scout( self, dry_run: bool, rna_dna_collections: list[RNADNACollection] ) -> None: for rna_dna_collection in rna_dna_collections: - rna_sample_internal_id: str = rna_dna_collection.rna_sample_internal_id + rna_sample_internal_id: str = rna_dna_collection.rna_sample_id dna_sample_name: str = rna_dna_collection.dna_sample_name for dna_case_id in rna_dna_collection.dna_case_ids: LOG.info( @@ -406,7 +391,7 @@ def upload_rna_fraser_outrider_to_scout( """Upload omics fraser and outrider file for a case to Scout.""" status_db: Store = self.status_db for rna_dna_collection in rna_dna_collections: - rna_sample_internal_id: str = rna_dna_collection.rna_sample_internal_id + rna_sample_internal_id: str = rna_dna_collection.rna_sample_id dna_sample_name: str = rna_dna_collection.dna_sample_name rna_fraser: File | None = self.get_rna_omics_fraser(case_id=case_id) rna_outrider: File | None = self.get_rna_omics_outrider(case_id=case_id) @@ -442,7 +427,7 @@ def upload_rna_fraser_outrider_to_scout( def upload_rna_genome_build_to_scout( self, dry_run: bool, - rna_case: str, + rna_case: Case, rna_dna_collections: list[RNADNACollection], ) -> None: """Upload RNA genome built for a RNA/DNA case to Scout.""" @@ -502,9 +487,11 @@ def upload_splice_junctions_bed_to_scout(self, dry_run: bool, case_id: str) -> N status_db: Store = self.status_db rna_case: Case = status_db.get_case_by_internal_id(case_id) - rna_dna_collections: list[RNADNACollection] = self.create_rna_dna_collections(rna_case) + rna_dna_collections: list[RNADNACollection] = ( + self.status_db.get_related_dna_cases_with_samples(rna_case) + ) for rna_dna_collection in rna_dna_collections: - rna_sample_internal_id: str = rna_dna_collection.rna_sample_internal_id + rna_sample_internal_id: str = rna_dna_collection.rna_sample_id dna_sample_name: str = rna_dna_collection.dna_sample_name splice_junctions_bed: File | None = self.get_splice_junctions_bed( case_id=case_id, sample_id=rna_sample_internal_id @@ -615,7 +602,9 @@ def upload_rna_omics_to_scout(self, dry_run: bool, case_id: str) -> None: """Upload RNA omics files to Scout.""" status_db: Store = self.status_db rna_case = status_db.get_case_by_internal_id(case_id) - rna_dna_collections: list[RNADNACollection] = self.create_rna_dna_collections(rna_case) + rna_dna_collections: list[RNADNACollection] = ( + self.status_db.get_related_dna_cases_with_samples(rna_case) + ) self.upload_omics_sample_id_to_scout( dry_run=dry_run, rna_dna_collections=rna_dna_collections ) @@ -675,45 +664,6 @@ def get_config_builder(self, analysis, hk_version) -> ScoutConfigBuilder: return config_builders[analysis.workflow] - def create_rna_dna_collections(self, rna_case: Case) -> list[RNADNACollection]: - return [self.create_rna_dna_collection(link.sample) for link in rna_case.links] - - def create_rna_dna_collection(self, rna_sample: Sample) -> RNADNACollection: - """Creates a collection containing the given RNA sample id, its related DNA sample name, and - a list of ids for the DNA cases connected to the DNA sample.""" - if not rna_sample.subject_id: - raise CgDataError( - f"Failed to link RNA sample {rna_sample.internal_id} to DNA samples - subject_id field is empty." - ) - - collaborators: set[Customer] = rna_sample.customer.collaborators - subject_id_samples: list[Sample] = ( - self.status_db.get_samples_by_customer_ids_and_subject_id_and_is_tumour( - customer_ids=[customer.id for customer in collaborators], - subject_id=rna_sample.subject_id, - is_tumour=rna_sample.is_tumour, - ) - ) - - subject_id_dna_samples: list[Sample] = self._get_application_prep_category( - subject_id_samples - ) - - if len(subject_id_dna_samples) != 1: - raise CgDataError( - f"Failed to upload files for RNA case: unexpected number of DNA sample matches for subject_id: " - f"{rna_sample.subject_id}. Number of matches: {len(subject_id_dna_samples)} " - ) - dna_sample: Sample = subject_id_dna_samples[0] - dna_cases: list[str] = self._dna_cases_related_to_dna_sample( - dna_sample=dna_sample, collaborators=collaborators - ) - return RNADNACollection( - rna_sample_internal_id=rna_sample.internal_id, - dna_sample_name=dna_sample.name, - dna_case_ids=dna_cases, - ) - def _dna_cases_related_to_dna_sample( self, dna_sample: Sample, collaborators: set[Customer] ) -> list[str]: @@ -768,11 +718,6 @@ def _get_application_prep_category( def get_related_uploaded_dna_cases(self, rna_case_id: str) -> set[str]: """Returns all uploaded DNA cases related to the specified RNA case.""" - unique_dna_case_ids: set[str] = self.get_unique_dna_cases_related_to_rna_case(rna_case_id) - uploaded_dna_cases: set[str] = set() - for dna_case_id in unique_dna_case_ids: - if self.status_db.get_case_by_internal_id(dna_case_id).is_uploaded: - uploaded_dna_cases.add(dna_case_id) - else: - LOG.warning(f"Related DNA case {dna_case_id} has not been completed.") - return uploaded_dna_cases + rna_case: Case = self.status_db.get_case_by_internal_id(rna_case_id) + dna_cases: list[Case] = self.status_db.get_uploaded_related_dna_cases(rna_case) + return {dna_case.internal_id for dna_case in dna_cases} diff --git a/cg/store/api/data_classes.py b/cg/store/api/data_classes.py new file mode 100644 index 0000000000..7f6a72812f --- /dev/null +++ b/cg/store/api/data_classes.py @@ -0,0 +1,11 @@ +from pydantic.dataclasses import dataclass + + +@dataclass +class RNADNACollection: + """Contains the id for an RNA sample, the name of its connected DNA sample, + and a list of connected, uploaded DNA cases.""" + + rna_sample_id: str + dna_sample_name: str + dna_case_ids: list[str] diff --git a/cg/store/crud/read.py b/cg/store/crud/read.py index 28f3c47553..9efd259859 100644 --- a/cg/store/crud/read.py +++ b/cg/store/crud/read.py @@ -15,11 +15,12 @@ SampleType, ) from cg.constants.sequencing import DNA_PREP_CATEGORIES, SeqLibraryPrepCategory -from cg.exc import CaseNotFoundError, CgError, OrderNotFoundError, SampleNotFoundError +from cg.exc import CaseNotFoundError, CgDataError, CgError, OrderNotFoundError, SampleNotFoundError from cg.models.orders.constants import OrderType from cg.models.orders.sample_base import SexEnum from cg.server.dto.samples.collaborator_samples_request import CollaboratorSamplesRequest from cg.services.orders.order_service.models import OrderQueryParams +from cg.store.api.data_classes import RNADNACollection from cg.store.base import BaseHandler from cg.store.exc import EntryNotFoundError from cg.store.filters.status_analysis_filters import AnalysisFilter, apply_analysis_filter @@ -677,24 +678,6 @@ def get_samples_by_customer_and_subject_id( customer_internal_id=customer_internal_id, subject_id=subject_id ).all() - def get_samples_by_customer_ids_and_subject_id_and_is_tumour( - self, customer_ids: list[int], subject_id: str, is_tumour: bool - ) -> list[Sample]: - """Return a list of samples matching a list of customers with given subject id and is a tumour or not.""" - samples = self._get_query(table=Sample) - filter_functions = [ - SampleFilter.BY_CUSTOMER_ENTRY_IDS, - SampleFilter.BY_SUBJECT_ID, - SampleFilter.BY_TUMOUR, - ] - return apply_sample_filter( - samples=samples, - customer_entry_ids=customer_ids, - subject_id=subject_id, - is_tumour=is_tumour, - filter_functions=filter_functions, - ).all() - def get_samples_by_any_id(self, **identifiers: dict) -> Query: """Return a sample query filtered by the given names and values of Sample attributes.""" samples: Query = self._get_query(table=Sample).order_by(Sample.internal_id.desc()) @@ -1643,8 +1626,12 @@ def _get_related_samples_query( prep_categories: list[SeqLibraryPrepCategory], collaborators: set[Customer], ) -> Query: - """Returns a sample query with the same subject_id, tumour status and within the collaborators of the given - sample and within the given list of prep categories.""" + """ + Returns a sample query with the same subject_id, tumour status and within the collaborators of the given + sample and within the given list of prep categories. + Raises: + CgDataError if the number of samples matching the criteria is not 1 + """ sample_application_version_query: Query = self._get_join_sample_application_version_query() @@ -1665,48 +1652,115 @@ def _get_related_samples_query( SampleFilter.BY_CUSTOMER_ENTRY_IDS, ], ) + if samples.count() != 1: + samples: list[Sample] = samples.all() + raise CgDataError( + f"No unique DNA sample could be found: found {len(samples)} samples: {[sample.internal_id for sample in samples]}" + ) return samples def get_uploaded_related_dna_cases(self, rna_case: Case) -> list[Case]: - """Returns all uploaded DNA cases ids related to the given RNA case.""" + """ + Finds all cases fulfilling the following criteria: + 1. The case should be uploaded + 2. The case should belong to a customer within the collaboration of the provided case's + 3. It should contain exactly one DNA sample matching one of the provided case's RNA samples in the following way: + 1. The DNA sample and the RNA sample should have matching subject_ids + 2. The DNA sample and the RNA sample should have matching is_tumour + 4. The DNA sample found in 3. should also: + 1. Have an application within a DNA prep category + 2. Belong to a customer within the provided collaboration + + Raises: + CgDataError if no related DNA cases are found + """ related_dna_cases: list[Case] = [] + collaborators: set[Customer] = rna_case.customer.collaborators for rna_sample in rna_case.samples: - - collaborators: set[Customer] = rna_sample.customer.collaborators - - related_dna_samples_query: Query = self._get_related_samples_query( - sample=rna_sample, - prep_categories=DNA_PREP_CATEGORIES, - collaborators=collaborators, + uploaded_dna_cases: list[Case] = self._get_related_uploaded_cases_for_rna_sample( + rna_sample=rna_sample, collaborators=collaborators ) - - dna_samples_cases_analysis_query: Query = ( - related_dna_samples_query.join(Sample.links).join(CaseSample.case).join(Analysis) + related_dna_cases.extend(uploaded_dna_cases) + if not related_dna_cases: + raise CgDataError( + f"No matching uploaded DNA cases for case {rna_case.internal_id} ({rna_case.name})." ) + return related_dna_cases - dna_samples_cases_analysis_query: Query = apply_case_filter( - cases=dna_samples_cases_analysis_query, - workflows=DNA_WORKFLOWS_WITH_SCOUT_UPLOAD, - customer_entry_ids=[customer.id for customer in collaborators], - filter_functions=[ - CaseFilter.BY_WORKFLOWS, - CaseFilter.BY_CUSTOMER_ENTRY_IDS, - ], + def _get_related_uploaded_cases_for_rna_sample( + self, rna_sample: Sample, collaborators: set[Customer] + ) -> list[Case]: + if not rna_sample.subject_id: + raise CgDataError( + f"Failed to link RNA sample {rna_sample.internal_id} to DNA samples - subject_id field is empty." ) - uploaded_dna_cases: list[Case] = ( - apply_analysis_filter( - analyses=dna_samples_cases_analysis_query, - filter_functions=[AnalysisFilter.IS_UPLOADED], - ) - .with_entities(Case) - .all() + related_dna_samples_query: Query = self._get_related_samples_query( + sample=rna_sample, + prep_categories=DNA_PREP_CATEGORIES, + collaborators=collaborators, + ) + customer_ids: list[int] = [customer.id for customer in collaborators] + return self._get_uploaded_dna_cases( + sample_query=related_dna_samples_query, customer_ids=customer_ids + ) + + def _get_uploaded_dna_cases(self, sample_query: Query, customer_ids: list[int]) -> list[Case]: + """Filters the provided sample_query on the customer_ids, DNA workflows supporting + Scout uploads and on cases having an uploaded analysis. Returns the matching cases.""" + dna_samples_cases_analysis_query: Query = ( + sample_query.join(Sample.links).join(CaseSample.case).join(Analysis) + ) + dna_samples_cases_analysis_query: Query = apply_case_filter( + cases=dna_samples_cases_analysis_query, + workflows=DNA_WORKFLOWS_WITH_SCOUT_UPLOAD, + customer_entry_ids=customer_ids, + filter_functions=[ + CaseFilter.BY_WORKFLOWS, + CaseFilter.BY_CUSTOMER_ENTRY_IDS, + ], + ) + uploaded_dna_cases: list[Case] = ( + apply_analysis_filter( + analyses=dna_samples_cases_analysis_query, + filter_functions=[AnalysisFilter.IS_UPLOADED], ) + .with_entities(Case) + .all() + ) + return uploaded_dna_cases - related_dna_cases.extend([case for case in uploaded_dna_cases]) - if not related_dna_cases: - raise CaseNotFoundError( - f"No matching uploaded DNA cases for case {rna_case.internal_id} ({rna_case.name})." + def get_related_dna_cases_with_samples(self, rna_case: Case) -> list[RNADNACollection]: + """ + Finds all cases fulfilling the following criteria: + 1. The case should be uploaded + 2. The case should belong to a customer within the collaboration of the provided case's + 3. It should contain exactly one DNA sample matching one of the provided case's RNA samples in the following way: + 1. The DNA sample and the RNA sample should have matching subject_ids + 2. The DNA sample and the RNA sample should have matching is_tumour + 4. The DNA sample found in 3. should also: + 1. Have an application within a DNA prep category + 2. Belong to a customer within the provided collaboration + + The cases are bundled by the DNA sample found in 3. + """ + collaborators: set[Customer] = rna_case.customer.collaborators + collaborator_ids: list[int] = [collaborator.id for collaborator in collaborators] + rna_dna_collections: list[RNADNACollection] = [] + for sample in rna_case.samples: + related_dna_samples: Query = self._get_related_samples_query( + sample=sample, prep_categories=DNA_PREP_CATEGORIES, collaborators=collaborators ) - return related_dna_cases + dna_sample_name: str = related_dna_samples.first().name + dna_cases: list[Case] = self._get_uploaded_dna_cases( + sample_query=related_dna_samples, customer_ids=collaborator_ids + ) + dna_case_ids: list[str] = [case.internal_id for case in dna_cases] + collection = RNADNACollection( + rna_sample_id=sample.internal_id, + dna_sample_name=dna_sample_name, + dna_case_ids=dna_case_ids, + ) + rna_dna_collections.append(collection) + return rna_dna_collections diff --git a/tests/meta/upload/scout/test_meta_upload_scoutapi_rna.py b/tests/meta/upload/scout/test_meta_upload_scoutapi_rna.py index b1f9a64852..413da237c1 100644 --- a/tests/meta/upload/scout/test_meta_upload_scoutapi_rna.py +++ b/tests/meta/upload/scout/test_meta_upload_scoutapi_rna.py @@ -559,8 +559,8 @@ def test_create_rna_dna_collections( # WHEN running the method to create a list of RNADNACollections # with the relationships between RNA/DNA samples and DNA cases - rna_dna_collections: list[RNADNACollection] = upload_scout_api.create_rna_dna_collections( - rna_case + rna_dna_collections: list[RNADNACollection] = ( + upload_scout_api.status_db.get_related_dna_cases_with_samples(rna_case) ) # THEN the output should be a list of RNADNACollections @@ -585,7 +585,7 @@ def test_add_rna_sample( # WHEN running the method to create a list of RNADNACollections # with the relationships between RNA/DNA samples and DNA cases - rna_dna_collections: list[RNADNACollection] = upload_scout_api.create_rna_dna_collections( + rna_dna_collections: list[RNADNACollection] = rna_store.get_related_dna_cases_with_samples( rna_case ) @@ -593,7 +593,7 @@ def test_add_rna_sample( assert rna_sample_list for sample in rna_sample_list: assert sample.internal_id in [ - rna_dna_collection.rna_sample_internal_id for rna_dna_collection in rna_dna_collections + rna_dna_collection.rna_sample_id for rna_dna_collection in rna_dna_collections ] @@ -609,13 +609,15 @@ def test_link_rna_sample_to_dna_sample( rna_sample: Sample = rna_store.get_sample_by_internal_id(rna_sample_son_id) # WHEN creating an RNADNACollection for an RNA sample - rna_dna_collection: RNADNACollection = upload_scout_api.create_rna_dna_collection(rna_sample) + rna_dna_collection: list[RNADNACollection] = rna_store.get_related_dna_cases_with_samples( + rna_sample.links[0].case + ) # THEN the RNADNACollection should contain the RNA sample - assert rna_sample_son_id == rna_dna_collection.rna_sample_internal_id + assert rna_sample_son_id == rna_dna_collection[0].rna_sample_id # THEN the RNADNACollection should have its dna_sample_id set to the related dna_sample_id - assert dna_sample_son_id == rna_dna_collection.dna_sample_name + assert dna_sample_son_id == rna_dna_collection[0].dna_sample_name def test_add_dna_cases_to_dna_sample( @@ -632,10 +634,12 @@ def test_add_dna_cases_to_dna_sample( dna_case: Case = rna_store.get_case_by_internal_id(internal_id=dna_case_id) # WHEN adding creating the RNADNACollection - rna_dna_collection: RNADNACollection = upload_scout_api.create_rna_dna_collection(rna_sample) + rna_dna_collection: list[RNADNACollection] = rna_store.get_related_dna_cases_with_samples( + rna_sample.links[0].case + ) # THEN the DNA cases should contain the DNA_case name associated with the DNA sample - assert dna_case.internal_id in rna_dna_collection.dna_case_ids + assert dna_case.internal_id in rna_dna_collection[0].dna_case_ids def test_map_dna_cases_to_dna_sample_incorrect_workflow( @@ -776,8 +780,8 @@ def test_upload_rna_multiqc_report_to_successful_dna_case_in_scout( ) # WHEN finding the related DNA case - dna_case_ids: Set[str] = upload_mip_analysis_scout_api.get_unique_dna_cases_related_to_rna_case( - case_id=rna_case_id + dna_case_ids: Set[str] = upload_mip_analysis_scout_api.get_related_uploaded_dna_cases( + rna_case_id ) # THEN the api should know that it should find related DNA cases @@ -802,7 +806,7 @@ def test_upload_tomte_rna_multiqc_report_to_successful_dna_case_in_scout( caplog.set_level(logging.INFO) - # GIVEN an RNA case, and an store with an rna connected to it + # GIVEN an RNA case, and a store with an rna connected to it upload_mip_analysis_scout_api.status_db: Store = rna_store # GIVEN an RNA case with a multiqc-htlml report @@ -819,8 +823,8 @@ def test_upload_tomte_rna_multiqc_report_to_successful_dna_case_in_scout( ) # WHEN finding the related DNA case - dna_case_ids: Set[str] = upload_mip_analysis_scout_api.get_unique_dna_cases_related_to_rna_case( - case_id=rna_case_id + dna_case_ids: Set[str] = upload_mip_analysis_scout_api.get_related_uploaded_dna_cases( + rna_case_id ) # THEN the api should know that it should find related DNA cases @@ -862,8 +866,8 @@ def test_upload_rna_delivery_report_to_successful_dna_case_in_scout( ) # WHEN finding the related DNA case - dna_case_ids: Set[str] = upload_mip_analysis_scout_api.get_unique_dna_cases_related_to_rna_case( - case_id=rna_case_id + dna_case_ids: Set[str] = upload_mip_analysis_scout_api.get_related_uploaded_dna_cases( + rna_case_id ) # THEN the api should know that it should find related DNA cases @@ -905,16 +909,8 @@ def test_upload_tomte_rna_delivery_report_to_successful_dna_case_in_scout( ) # WHEN finding the related DNA case - dna_case_ids: Set[str] = ( - upload_tomte_analysis_scout_api.get_unique_dna_cases_related_to_rna_case( - case_id=rna_case_id - ) - ) - - dna_case_ids: Set[str] = ( - upload_tomte_analysis_scout_api.get_unique_dna_cases_related_to_rna_case( - case_id=rna_case_id - ) + dna_case_ids: Set[str] = upload_tomte_analysis_scout_api.get_related_uploaded_dna_cases( + rna_case_id ) # THEN the dna case id should have been mentioned in the logging (and used in the upload) @@ -956,8 +952,8 @@ def test_upload_rna_report_to_not_yet_uploaded_dna_case_in_scout( )[0] # WHEN finding the related DNA case with no successful upload - dna_case_ids: Set[str] = upload_mip_analysis_scout_api.get_unique_dna_cases_related_to_rna_case( - case_id=rna_case_id + dna_case_ids: Set[str] = upload_mip_analysis_scout_api.get_related_uploaded_dna_cases( + rna_case_id ) dna_case: Case = rna_store.get_case_by_internal_id(internal_id=list(dna_case_ids)[0]) dna_case.analyses[0].uploaded_at = None diff --git a/tests/store/crud/conftest.py b/tests/store/crud/conftest.py index d884fc947d..fe1b519d70 100644 --- a/tests/store/crud/conftest.py +++ b/tests/store/crud/conftest.py @@ -146,25 +146,6 @@ def store_with_rna_and_dna_samples_and_cases(store: Store, helpers: StoreHelpers customer_id="cust001", ) - helpers.add_sample( - store=store, - internal_id="related_dna_sample_2", - application_tag=SeqLibraryPrepCategory.TARGETED_GENOME_SEQUENCING.value, - application_type=SeqLibraryPrepCategory.TARGETED_GENOME_SEQUENCING.value, - subject_id="subject_1", - is_tumour=True, - customer_id="cust000", - ) - helpers.add_sample( - store=store, - internal_id="related_dna_sample_3", - application_tag=SeqLibraryPrepCategory.WHOLE_EXOME_SEQUENCING.value, - application_type=SeqLibraryPrepCategory.WHOLE_EXOME_SEQUENCING.value, - subject_id="subject_1", - is_tumour=True, - customer_id="cust000", - ) - helpers.add_sample( store=store, internal_id="not_related_dna_sample", @@ -238,22 +219,7 @@ def related_dna_samples(store_with_rna_and_dna_samples_and_cases: Store) -> list ) ) - related_dna_sample_2: Sample = ( - store_with_rna_and_dna_samples_and_cases.get_sample_by_internal_id( - internal_id="related_dna_sample_2" - ) - ) - related_dna_sample_3: Sample = ( - store_with_rna_and_dna_samples_and_cases.get_sample_by_internal_id( - internal_id="related_dna_sample_3" - ) - ) - - return [ - related_dna_sample_1, - related_dna_sample_2, - related_dna_sample_3, - ] + return [related_dna_sample_1] @pytest.fixture diff --git a/tests/store/crud/read/test_read_sample.py b/tests/store/crud/read/test_read_sample.py index e2ce710ba1..61e7503080 100644 --- a/tests/store/crud/read/test_read_sample.py +++ b/tests/store/crud/read/test_read_sample.py @@ -115,71 +115,6 @@ def test_get_samples_by_subject_id( assert samples and len(samples) == 2 -def test_get_samples_by_customer_id_list_and_subject_id_and_is_tumour( - store_with_samples_customer_id_and_subject_id_and_tumour_status: Store, - customer_ids: list[int] = [1, 2], - subject_id: str = "test_subject", - is_tumour: bool = True, -): - """Test that samples can be fetched by customer ID, subject ID, and tumour status.""" - # GIVEN a database with four samples, two with customer ID 1 and two with customer ID 2 - - # ASSERT that there are customers with the given customer IDs - for customer_id in customer_ids: - assert store_with_samples_customer_id_and_subject_id_and_tumour_status.get_customer_by_internal_id( - customer_internal_id=str(customer_id) - ) - - # WHEN fetching the samples by customer ID list, subject ID, and tumour status - samples = store_with_samples_customer_id_and_subject_id_and_tumour_status.get_samples_by_customer_ids_and_subject_id_and_is_tumour( - customer_ids=customer_ids, subject_id=subject_id, is_tumour=is_tumour - ) - - # THEN two samples should be returned, one for each customer ID, with the specified subject ID and tumour status - assert isinstance(samples, list) - assert len(samples) == 2 - - for sample in samples: - assert isinstance(sample, Sample) - - for customer_id, sample in zip(customer_ids, samples): - assert sample.customer_id == customer_id - assert sample.subject_id == subject_id - assert sample.is_tumour == is_tumour - - -def test_get_samples_by_customer_id_list_and_subject_id_and_is_tumour_with_non_existing_customer_id( - store_with_samples_customer_id_and_subject_id_and_tumour_status: Store, -): - """Test that no samples are returned when filtering on non-existing customer ID.""" - # GIVEN a database with four samples, two with customer ID 1 and two with customer ID 2 - - # ASSERT that there are no customers with the given customer IDs - customer_ids = [1, 2, 3] - for customer_id in customer_ids: - if customer_id == 3: - assert ( - store_with_samples_customer_id_and_subject_id_and_tumour_status.get_customer_by_internal_id( - customer_internal_id=str(customer_id) - ) - is None - ) - else: - assert store_with_samples_customer_id_and_subject_id_and_tumour_status.get_customer_by_internal_id( - customer_internal_id=str(customer_id) - ) - - # WHEN fetching the samples by customer ID list, subject ID, and tumour status - non_existing_customer_id = [3] - samples = store_with_samples_customer_id_and_subject_id_and_tumour_status.get_samples_by_customer_ids_and_subject_id_and_is_tumour( - customer_ids=non_existing_customer_id, subject_id="test_subject", is_tumour=True - ) - - # THEN no samples should be returned - assert isinstance(samples, list) - assert len(samples) == 0 - - def test_get_sample_by_name(store_with_samples_that_have_names: Store, name="test_sample_1"): """Test that samples can be fetched by name.""" # GIVEN a database with two samples of which one has a name From e0648abde2b7455c5c971d89305ed1bd088d150b Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Thu, 30 Jan 2025 13:35:37 +0000 Subject: [PATCH 12/20] =?UTF-8?q?Bump=20version:=2067.0.14=20=E2=86=92=206?= =?UTF-8?q?7.0.15=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cg/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c276e65d24..1720a527ad 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 67.0.14 +current_version = 67.0.15 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index f32e8e32ba..0613a6e806 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "67.0.14" +__version__ = "67.0.15" diff --git a/pyproject.toml b/pyproject.toml index 3d192e86a4..f548c06fa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "cg" -version = "67.0.14" +version = "67.0.15" description = "Clinical Genomics command center" readme = {file = "README.md", content-type = "text/markdown"} homepage = "https://github.com/Clinical-Genomics/cg" From 49ccde3e9d2f816da07204b279594dc6ab9a463f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20Ohlsson=20=C3=85ngnell?= <40887124+islean@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:14:58 +0100 Subject: [PATCH 13/20] Patch Tomte delivery messages (#4172) (patch) ### Fixed - Tomte delivery messages use the same logic as MIP-RNA --- cg/services/delivery_message/utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cg/services/delivery_message/utils.py b/cg/services/delivery_message/utils.py index f3cfa87c41..b8e398aa5e 100644 --- a/cg/services/delivery_message/utils.py +++ b/cg/services/delivery_message/utils.py @@ -16,12 +16,12 @@ from cg.services.delivery_message.messages.fastq_analysis_message import FastqAnalysisMessage from cg.services.delivery_message.messages.microsalt_mwx_message import MicrosaltMwxMessage from cg.services.delivery_message.messages.rna_delivery_message import ( - RNAScoutStrategy, - RNAFastqStrategy, RNAAnalysisStrategy, + RNADeliveryMessage, RNAFastqAnalysisStrategy, + RNAFastqStrategy, + RNAScoutStrategy, RNAUploadMessageStrategy, - RNADeliveryMessage, ) from cg.store.models import Case, Sample from cg.store.store import Store @@ -60,7 +60,7 @@ def get_message_strategy(case: Case, store: Store) -> DeliveryMessage: if case.data_analysis == Workflow.MUTANT: return CovidMessage() - if case.data_analysis == Workflow.MIP_RNA: + if case.data_analysis in [Workflow.MIP_RNA, Workflow.TOMTE]: return get_rna_message_strategy_from_data_delivery(case=case, store=store) message_strategy: DeliveryMessage = get_message_strategy_from_data_delivery(case) @@ -79,8 +79,7 @@ def get_rna_message_strategy_from_data_delivery( If a scout delivery is required it will use the RNADeliveryMessage class that links RNA to DNA cases. Otherwise it used the conventional delivery message strategy. """ - message_strategy = RNA_STRATEGY_MAP[case.data_delivery] - if message_strategy: + if message_strategy := RNA_STRATEGY_MAP.get(case.data_delivery): return RNADeliveryMessage(store=store, strategy=message_strategy()) return MESSAGE_MAP[case.data_delivery]() From 04f47869b39a2025e5810df3ed3b129bb33165d5 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Thu, 30 Jan 2025 15:15:23 +0000 Subject: [PATCH 14/20] =?UTF-8?q?Bump=20version:=2067.0.15=20=E2=86=92=206?= =?UTF-8?q?7.0.16=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cg/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1720a527ad..0822ed2d69 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 67.0.15 +current_version = 67.0.16 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index 0613a6e806..b6ac843b5b 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "67.0.15" +__version__ = "67.0.16" diff --git a/pyproject.toml b/pyproject.toml index f548c06fa6..3818554298 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "cg" -version = "67.0.15" +version = "67.0.16" description = "Clinical Genomics command center" readme = {file = "README.md", content-type = "text/markdown"} homepage = "https://github.com/Clinical-Genomics/cg" From f1b1e2b110a78073100810b6bf6778b579f7bda8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20Ohlsson=20=C3=85ngnell?= <40887124+islean@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:26:06 +0100 Subject: [PATCH 15/20] Fix Pydantic error mapping (#4170) (patch) ### Fixed - Determining the type of order/case/sample/case_sample error is not by length of loc --- .../validation/model_validator/utils.py | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/cg/services/orders/validation/model_validator/utils.py b/cg/services/orders/validation/model_validator/utils.py index a4e736e955..0d290f6a54 100644 --- a/cg/services/orders/validation/model_validator/utils.py +++ b/cg/services/orders/validation/model_validator/utils.py @@ -100,8 +100,15 @@ def create_case_sample_error(error: ErrorDetails) -> CaseSampleError: What follows below are ways of extracting data from a Pydantic ErrorDetails object. The aim is to find out where the error occurred, for which the 'loc' value (which is a tuple) can be used. It is generally structured in alternating strings and ints, specifying field names and list indices. An example: -if loc = ('cases', 3, 'samples', 2, 'well_position'), that means that the error stems from the well_position of the -third sample in the fourth case. +if loc = ('samples', 2, 'well_position'), that means that the error stems from the well_position of the +third sample in the order. + +As an additional point of complexity, the discriminator is also added to the loc, specifically in +OrdersWithCases which have a discriminator for both cases and samples specifying if it is a new +or existing case/sample. So + loc = ('cases', 0, 'new', 'priority') +means that the error concerns the first case in the order, which is a new case, and it concerns the field +'priority'. """ @@ -122,19 +129,19 @@ def get_order_error_details(error_details: list[ErrorDetails]) -> list[ErrorDeta def is_sample_error(error: ErrorDetails) -> bool: - return len(error["loc"]) == 3 and error["loc"][0] == "samples" + return error["loc"][0] == "samples" def is_case_error(error: ErrorDetails) -> bool: - return len(error["loc"]) == 4 and error["loc"][0] == "cases" + return "cases" in error["loc"] and "samples" not in error["loc"] def is_case_sample_error(error: ErrorDetails) -> bool: - return len(error["loc"]) == 7 + return "cases" in error["loc"] and "samples" in error["loc"] def is_order_error(error: ErrorDetails) -> bool: - return len(error["loc"]) == 1 + return error["loc"][0] not in ["cases", "samples"] def get_error_message(error: ErrorDetails) -> str: @@ -142,15 +149,18 @@ def get_error_message(error: ErrorDetails) -> str: def get_sample_field_name(error: ErrorDetails) -> str: - return error["loc"][2] + index_for_field_name: int = error["loc"].index("samples") + 2 + return error["loc"][index_for_field_name] def get_case_field_name(error: ErrorDetails) -> str: - return error["loc"][3] + index_for_field_name: int = error["loc"].index("cases") + 3 + return error["loc"][index_for_field_name] def get_case_sample_field_name(error: ErrorDetails) -> str: - return error["loc"][6] + index_for_field_name: int = error["loc"].index("samples") + 3 + return error["loc"][index_for_field_name] def get_order_field_name(error: ErrorDetails) -> str: @@ -158,12 +168,15 @@ def get_order_field_name(error: ErrorDetails) -> str: def get_sample_index(error: ErrorDetails) -> int: - return error["loc"][1] + index_for_index: int = error["loc"].index("samples") + 1 + return error["loc"][index_for_index] def get_case_index(error: ErrorDetails) -> int: - return error["loc"][1] + index_for_index: int = error["loc"].index("cases") + 1 + return error["loc"][index_for_index] def get_case_sample_index(error: ErrorDetails) -> int: - return error["loc"][4] + index_for_index: int = error["loc"].index("samples") + 1 + return error["loc"][index_for_index] From 9ad92dad076793c8267f0ebf3ab3ad132b0765a4 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Thu, 30 Jan 2025 15:26:38 +0000 Subject: [PATCH 16/20] =?UTF-8?q?Bump=20version:=2067.0.16=20=E2=86=92=206?= =?UTF-8?q?7.0.17=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cg/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 0822ed2d69..25139f1f8a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 67.0.16 +current_version = 67.0.17 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index b6ac843b5b..68f0694318 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "67.0.16" +__version__ = "67.0.17" diff --git a/pyproject.toml b/pyproject.toml index 3818554298..365c13ab8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "cg" -version = "67.0.16" +version = "67.0.17" description = "Clinical Genomics command center" readme = {file = "README.md", content-type = "text/markdown"} homepage = "https://github.com/Clinical-Genomics/cg" From e0e725e0a213927b677e5baf939fea783824bddb Mon Sep 17 00:00:00 2001 From: Sebastian Diaz Date: Mon, 3 Feb 2025 09:31:55 +0100 Subject: [PATCH 17/20] Adds explicit left to right validation metric models (#4165)(patch) ## Description Closes https://github.com/Clinical-Genomics/cg/issues/4078 States explicitly that pydantic works with backward compatibility for validating union typing in the `NextflowAnalysis` class. ### Added ### Changed - the union typing of the `NextflowAnalysis` attribute `smaple_metrics` and explicitly specify to choose union models according to `left-to-right` --- cg/models/analysis.py | 11 +++++++---- cg/models/taxprofiler/taxprofiler.py | 5 +++-- tests/meta/workflow/test_rnafusion.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cg/models/analysis.py b/cg/models/analysis.py index c2e3eb8b80..9cf50f63c4 100644 --- a/cg/models/analysis.py +++ b/cg/models/analysis.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, Field from cg.models.raredisease.raredisease import RarediseaseQCMetrics from cg.models.rnafusion.rnafusion import RnafusionQCMetrics @@ -15,6 +15,9 @@ class AnalysisModel(BaseModel): class NextflowAnalysis(AnalysisModel): """Nextflow's analysis results model.""" - sample_metrics: dict[ - str, RarediseaseQCMetrics | RnafusionQCMetrics | TaxprofilerQCMetrics | TomteQCMetrics - ] + sample_metrics: ( + dict[str, RarediseaseQCMetrics] + | dict[str, RnafusionQCMetrics] + | dict[str, TaxprofilerQCMetrics] + | dict[str, TomteQCMetrics] + ) = Field(union_mode="left_to_right") diff --git a/cg/models/taxprofiler/taxprofiler.py b/cg/models/taxprofiler/taxprofiler.py index fbf585a27a..ed3433a3f3 100644 --- a/cg/models/taxprofiler/taxprofiler.py +++ b/cg/models/taxprofiler/taxprofiler.py @@ -1,12 +1,13 @@ from pathlib import Path -from pydantic import BaseModel, Field +from pydantic import Field from cg.constants.sequencing import SequencingPlatform from cg.models.nf_analysis import NextflowSampleSheetEntry, WorkflowParameters +from cg.models.qc_metrics import QCMetrics -class TaxprofilerQCMetrics(BaseModel): +class TaxprofilerQCMetrics(QCMetrics): """Taxprofiler QC metrics.""" after_filtering_gc_content: float diff --git a/tests/meta/workflow/test_rnafusion.py b/tests/meta/workflow/test_rnafusion.py index 5ee071e03d..d88f257afb 100644 --- a/tests/meta/workflow/test_rnafusion.py +++ b/tests/meta/workflow/test_rnafusion.py @@ -21,7 +21,7 @@ def test_parse_analysis( qc_metrics: list[MetricsBase] = analysis_api.get_multiqc_json_metrics(case_id=rnafusion_case_id) # WHEN extracting the analysis model - analysis_model: NextflowAnalysis = analysis_api.parse_analysis(qc_metrics_raw=qc_metrics) + analysis_model: NextflowAnalysis = analysis_api.parse_analysis(qc_metrics) # THEN the analysis model and its content should have been correctly extracted assert analysis_model.sample_metrics[sample_id].model_dump() == rnafusion_metrics From 9b9c87c0afe0a6e6b6c5505467682a8df30bddd7 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Mon, 3 Feb 2025 08:32:20 +0000 Subject: [PATCH 18/20] =?UTF-8?q?Bump=20version:=2067.0.17=20=E2=86=92=206?= =?UTF-8?q?7.0.18=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cg/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 25139f1f8a..bd3da62130 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 67.0.17 +current_version = 67.0.18 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index 68f0694318..03febff38c 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "67.0.17" +__version__ = "67.0.18" diff --git a/pyproject.toml b/pyproject.toml index 365c13ab8e..f04cf03ee3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "cg" -version = "67.0.17" +version = "67.0.18" description = "Clinical Genomics command center" readme = {file = "README.md", content-type = "text/markdown"} homepage = "https://github.com/Clinical-Genomics/cg" From d8b84fa728d19464b8f858040a7b8fc773d38a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20Ohlsson=20=C3=85ngnell?= <40887124+islean@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:02:48 +0100 Subject: [PATCH 19/20] Set metagenome lims ids (#4175) (patch) ### Fixed - Set the LIMS internal id for metagenome orders --- .../orders/storing/implementations/metagenome_order_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cg/services/orders/storing/implementations/metagenome_order_service.py b/cg/services/orders/storing/implementations/metagenome_order_service.py index 325a049c5a..07d53d23d2 100644 --- a/cg/services/orders/storing/implementations/metagenome_order_service.py +++ b/cg/services/orders/storing/implementations/metagenome_order_service.py @@ -93,6 +93,7 @@ def _create_db_sample( sex=Sex.UNKNOWN, comment=sample.comment, control=sample.control, + internal_id=sample._generated_lims_id, order=order.name, ordered=datetime.now(), original_ticket=order._generated_ticket_id, From cfe857df5b7f455957d2a730fb8612fc88511042 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Mon, 3 Feb 2025 09:03:26 +0000 Subject: [PATCH 20/20] =?UTF-8?q?Bump=20version:=2067.0.18=20=E2=86=92=206?= =?UTF-8?q?7.0.19=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- cg/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index bd3da62130..536e109cb0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 67.0.18 +current_version = 67.0.19 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index 03febff38c..3d546a49c6 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "67.0.18" +__version__ = "67.0.19" diff --git a/pyproject.toml b/pyproject.toml index f04cf03ee3..22290b5667 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [project] name = "cg" -version = "67.0.18" +version = "67.0.19" description = "Clinical Genomics command center" readme = {file = "README.md", content-type = "text/markdown"} homepage = "https://github.com/Clinical-Genomics/cg"