From 3524c0ad8c9836c7c83ea0305460dadc539a78ec Mon Sep 17 00:00:00 2001 From: Henrik Stranneheim Date: Wed, 27 Nov 2024 09:00:27 +0100 Subject: [PATCH 01/25] feat(analysisType): Remove rna (#3973) ### Changed - Remove unused AnalysisType rna --- cg/constants/tb.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cg/constants/tb.py b/cg/constants/tb.py index 33e26a2e9a..c52267f818 100644 --- a/cg/constants/tb.py +++ b/cg/constants/tb.py @@ -16,6 +16,5 @@ class AnalysisTypes(StrEnum): WGS: str = "wgs" WES: str = "wes" TGS: str = "tgs" - RNA: str = "rna" WTS: str = "wts" OTHER: str = "other" From 13032407214e654216a0cd191f491bff8bc8e438 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Wed, 27 Nov 2024 08:00:54 +0000 Subject: [PATCH 02/25] =?UTF-8?q?Bump=20version:=2064.5.12=20=E2=86=92=206?= =?UTF-8?q?4.5.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 120c8e87cd..f23b078481 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 64.5.12 +current_version = 64.5.13 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index 35837b5217..e33450639f 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "64.5.12" +__version__ = "64.5.13" diff --git a/pyproject.toml b/pyproject.toml index 851fccb917..988b37df4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "cg" -version = "64.5.12" +version = "64.5.13" description = "Clinical Genomics command center" authors = ["Clinical Genomics "] readme = "README.md" From 9d83b5172b3a99e9018b927ea13a321b977d09c6 Mon Sep 17 00:00:00 2001 From: Annick Renevey <47788523+rannick@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:54:01 +0100 Subject: [PATCH 03/25] Set parameters in params file, and not config (#3965) | major - Rename workflow_path to workflow_bin_path to make clearer that we are talking about the codebase of the workflow - Renamed config_resources into resources, config_platform into platform - Split config_params into params and config - Building parameters with config_case now has a part with cg-built(or fetched) parameters that is added to the parameter file from servers. Placeholders are used and replaced by the value extracted from the same parameter dictionary, to avoid typos/repeats - Use target_bed_file instead of target_bed because target_bed needs the path to the reference directory that is defined in servers -> target_bed_file is the filename and target_bed in params uses that with the reference path to build the full file path - Removed unused is_params_appended_to_nextflow_config function - Add references are specified in params files from servers and don't need to be read-in and handled by cg --- cg/io/config.py | 15 --- cg/meta/workflow/nf_analysis.py | 99 ++++++++++++------- cg/meta/workflow/nf_handlers.py | 10 +- cg/meta/workflow/raredisease.py | 27 +++-- cg/meta/workflow/rnafusion.py | 23 ++--- cg/meta/workflow/taxprofiler.py | 9 +- cg/meta/workflow/tomte.py | 12 +-- cg/models/cg_config.py | 48 ++++----- cg/models/raredisease/raredisease.py | 3 +- cg/models/rnafusion/rnafusion.py | 4 - .../cli/generate/delivery_report/test_cli.py | 6 +- tests/conftest.py | 41 +++++--- ...e_params.config => pipeline_config.config} | 0 .../analysis/nf-analysis/pipeline_params.yaml | 1 + tests/io/test_io_config.py | 17 ---- 15 files changed, 141 insertions(+), 174 deletions(-) delete mode 100644 cg/io/config.py rename tests/fixtures/analysis/nf-analysis/{pipeline_params.config => pipeline_config.config} (100%) create mode 100644 tests/fixtures/analysis/nf-analysis/pipeline_params.yaml delete mode 100644 tests/io/test_io_config.py diff --git a/cg/io/config.py b/cg/io/config.py deleted file mode 100644 index 71f28e862c..0000000000 --- a/cg/io/config.py +++ /dev/null @@ -1,15 +0,0 @@ -from pathlib import Path -from typing import Any -from cg.constants.symbols import EMPTY_STRING - - -def write_config_nextflow_style(content: dict[str, Any] | None) -> str: - """Write content to stream accepted by Nextflow config files with non-quoted booleans and quoted strings.""" - string: str = EMPTY_STRING - double_quotes: str = '"' - for parameter, value in content.items(): - if isinstance(value, Path): - value: str = value.as_posix() - quotes = double_quotes if type(value) is str else EMPTY_STRING - string += f"params.{parameter} = {quotes}{value}{quotes}\n" - return string diff --git a/cg/meta/workflow/nf_analysis.py b/cg/meta/workflow/nf_analysis.py index d84dc509c5..7677b8397e 100644 --- a/cg/meta/workflow/nf_analysis.py +++ b/cg/meta/workflow/nf_analysis.py @@ -1,4 +1,6 @@ +import copy import logging +import re from datetime import datetime from pathlib import Path from typing import Any, Iterator @@ -20,11 +22,10 @@ from cg.constants.nf_analysis import NfTowerStatus from cg.constants.tb import AnalysisStatus from cg.exc import CgError, HousekeeperStoreError, MetricsQCError -from cg.io.config import write_config_nextflow_style from cg.io.controller import ReadFile, WriteFile from cg.io.json import read_json from cg.io.txt import concat_txt, write_txt -from cg.io.yaml import write_yaml_nextflow_style +from cg.io.yaml import read_yaml, write_yaml_nextflow_style from cg.meta.workflow.analysis import AnalysisAPI from cg.meta.workflow.nf_handlers import NextflowHandler, NfTowerHandler from cg.meta.workflow.utils.genome_build_helpers import get_genome_build @@ -55,14 +56,15 @@ def __init__(self, config: CGConfig, workflow: Workflow): super().__init__(workflow=workflow, config=config) self.workflow: Workflow = workflow self.root_dir: str | None = None - self.nfcore_workflow_path: str | None = None + self.workflow_bin_path: str | None = None self.references: str | None = None self.profile: str | None = None self.conda_env: str | None = None self.conda_binary: str | None = None - self.config_platform: str | None = None - self.config_params: str | None = None - self.config_resources: str | None = None + self.platform: str | None = None + self.params: str | None = None + self.config: str | None = None + self.resources: str | None = None self.tower_binary_path: str | None = None self.tower_workflow: str | None = None self.account: str | None = None @@ -98,11 +100,6 @@ def sample_sheet_headers(self) -> list[str]: """Headers for sample sheet.""" raise NotImplementedError - @property - def is_params_appended_to_nextflow_config(self) -> bool: - """Return True if parameters should be added into the nextflow config file instead of the params file.""" - return False - @property def is_multiqc_pattern_search_exact(self) -> bool: """Return True if only exact pattern search is allowed to collect metrics information from MultiQC file. @@ -131,24 +128,20 @@ def get_workflow_version(self, case_id: str) -> str: """Get workflow version from config.""" return self.revision - def get_workflow_parameters(self, case_id: str) -> WorkflowParameters: + def get_built_workflow_parameters(self, case_id: str) -> WorkflowParameters: """Return workflow parameters.""" raise NotImplementedError def get_nextflow_config_content(self, case_id: str) -> str: """Return nextflow config content.""" config_files_list: list[str] = [ - self.config_platform, - self.config_params, - self.config_resources, + self.platform, + self.config, + self.resources, ] extra_parameters_str: list[str] = [ self.set_cluster_options(case_id=case_id), ] - if self.is_params_appended_to_nextflow_config: - extra_parameters_str.append( - write_config_nextflow_style(self.get_workflow_parameters(case_id=case_id).dict()) - ) return concat_txt( file_paths=config_files_list, str_content=extra_parameters_str, @@ -284,12 +277,12 @@ def verify_deliverables_file_exists(self, case_id: str) -> None: if not Path(self.get_deliverables_file_path(case_id=case_id)).exists(): raise CgError(f"No deliverables file found for case {case_id}") - def write_params_file(self, case_id: str, workflow_parameters: dict = None) -> None: + def write_params_file(self, case_id: str, replaced_workflow_parameters: dict = None) -> None: """Write params-file for analysis.""" LOG.debug("Writing parameters file") - if workflow_parameters: + if replaced_workflow_parameters: write_yaml_nextflow_style( - content=workflow_parameters, + content=replaced_workflow_parameters, file_path=self.get_params_file_path(case_id=case_id), ) else: @@ -330,7 +323,7 @@ def write_trailblazer_config(self, case_id: str, tower_id: str) -> None: file_path=config_path, ) - def create_sample_sheet(self, case_id: str, dry_run: bool): + def create_sample_sheet(self, case_id: str, dry_run: bool) -> None: """Create sample sheet for a case.""" sample_sheet_content: list[list[Any]] = self.get_sample_sheet_content(case_id=case_id) if not dry_run: @@ -340,25 +333,59 @@ def create_sample_sheet(self, case_id: str, dry_run: bool): header=self.sample_sheet_headers, ) - def create_params_file(self, case_id: str, dry_run: bool): + def create_params_file(self, case_id: str, dry_run: bool) -> None: """Create parameters file for a case.""" - LOG.debug("Getting parameters information") - workflow_parameters = None - if not self.is_params_appended_to_nextflow_config: - workflow_parameters: dict | None = self.get_workflow_parameters(case_id=case_id).dict() + LOG.debug("Getting parameters information built on-the-fly") + built_workflow_parameters: dict | None = self.get_built_workflow_parameters( + case_id=case_id + ).model_dump() + LOG.debug("Adding parameters from the pipeline config file if it exist") + + workflow_parameters: dict = built_workflow_parameters | ( + read_yaml(self.params) if hasattr(self, "params") and self.params else {} + ) + replaced_workflow_parameters: dict = self.replace_values_in_params_file( + workflow_parameters=workflow_parameters + ) if not dry_run: - self.write_params_file(case_id=case_id, workflow_parameters=workflow_parameters) + self.write_params_file( + case_id=case_id, replaced_workflow_parameters=replaced_workflow_parameters + ) + + def replace_values_in_params_file(self, workflow_parameters: dict) -> dict: + replaced_workflow_parameters = copy.deepcopy(workflow_parameters) + """Iterate through the dictionary until all placeholders are replaced with the corresponding value from the dictionary""" + while True: + resolved: bool = True + for key, value in replaced_workflow_parameters.items(): + new_value: str | int = self.replace_params_placeholders(value, workflow_parameters) + if new_value != value: + resolved = False + replaced_workflow_parameters[key] = new_value + if resolved: + break + return replaced_workflow_parameters + + def replace_params_placeholders(self, value: str | int, workflow_parameters: dict) -> str: + """Replace values marked as placeholders with values from the given dictionary""" + if isinstance(value, str): + placeholders: list[str] = re.findall(r"{{\s*([^{}\s]+)\s*}}", value) + for placeholder in placeholders: + if placeholder in workflow_parameters: + value = value.replace( + f"{{{{{placeholder}}}}}", str(workflow_parameters[placeholder]) + ) + return value def create_nextflow_config(self, case_id: str, dry_run: bool = False) -> None: """Create nextflow config file.""" if content := self.get_nextflow_config_content(case_id=case_id): LOG.debug("Writing nextflow config file") - if dry_run: - return - write_txt( - content=content, - file_path=self.get_nextflow_config_path(case_id=case_id), - ) + if not dry_run: + write_txt( + content=content, + file_path=self.get_nextflow_config_path(case_id=case_id), + ) def create_gene_panel(self, case_id: str, dry_run: bool) -> None: """Create and write an aggregated gene panel file exported from Scout.""" @@ -402,7 +429,7 @@ def _run_analysis_with_nextflow( LOG.info("Workflow will be executed using Nextflow") parameters: list[str] = NextflowHandler.get_nextflow_run_parameters( case_id=case_id, - workflow_path=self.nfcore_workflow_path, + workflow_bin_path=self.workflow_bin_path, root_dir=self.root_dir, command_args=command_args.dict(), ) diff --git a/cg/meta/workflow/nf_handlers.py b/cg/meta/workflow/nf_handlers.py index c57e916d2f..0a91c138cf 100644 --- a/cg/meta/workflow/nf_handlers.py +++ b/cg/meta/workflow/nf_handlers.py @@ -6,11 +6,7 @@ from cg.apps.slurm.slurm_api import SlurmAPI from cg.constants.constants import FileExtensions, FileFormat -from cg.constants.nextflow import ( - JAVA_MEMORY_HEADJOB, - NXF_JVM_ARGS_ENV, - SlurmHeadJobDefaults, -) +from cg.constants.nextflow import JAVA_MEMORY_HEADJOB, NXF_JVM_ARGS_ENV, SlurmHeadJobDefaults from cg.io.controller import ReadFile from cg.models.slurm.sbatch import Sbatch from cg.utils.utils import build_command_from_dict @@ -116,7 +112,7 @@ def get_variables_to_export() -> dict[str, str]: @classmethod def get_nextflow_run_parameters( - cls, case_id: str, workflow_path: str, root_dir: str, command_args: dict + cls, case_id: str, workflow_bin_path: str, root_dir: str, command_args: dict ) -> list[str]: """Returns a Nextflow run command given a dictionary with arguments.""" @@ -137,7 +133,7 @@ def get_nextflow_run_parameters( ), exclude_true=True, ) - return nextflow_options + ["run", workflow_path] + run_options + return nextflow_options + ["run", workflow_bin_path] + run_options @staticmethod def get_head_job_sbatch_path(case_directory: Path) -> Path: diff --git a/cg/meta/workflow/raredisease.py b/cg/meta/workflow/raredisease.py index 98b62a97d9..35186e082a 100644 --- a/cg/meta/workflow/raredisease.py +++ b/cg/meta/workflow/raredisease.py @@ -49,14 +49,14 @@ def __init__( ): super().__init__(config=config, workflow=workflow) self.root_dir: str = config.raredisease.root - self.nfcore_workflow_path: str = config.raredisease.workflow_path - self.references: str = config.raredisease.references + self.workflow_bin_path: str = config.raredisease.workflow_bin_path self.profile: str = config.raredisease.profile self.conda_env: str = config.raredisease.conda_env self.conda_binary: str = config.raredisease.conda_binary - self.config_platform: str = config.raredisease.config_platform - self.config_params: str = config.raredisease.config_params - self.config_resources: str = config.raredisease.config_resources + self.platform: str = config.raredisease.platform + self.params: str = config.raredisease.params + self.config: str = config.raredisease.config + self.resources: str = config.raredisease.resources self.tower_binary_path: str = config.tower_binary_path self.tower_workflow: str = config.raredisease.tower_workflow self.account: str = config.raredisease.slurm.account @@ -96,31 +96,30 @@ def get_target_bed(self, case_id: str, analysis_type: str) -> str: """ Return the target bed file from LIMS and use default capture kit for WHOLE_GENOME_SEQUENCING. """ - target_bed: str = self.get_target_bed_from_lims(case_id=case_id) - if not target_bed: + target_bed_file: str = self.get_target_bed_from_lims(case_id=case_id) + if not target_bed_file: if analysis_type == AnalysisType.WHOLE_GENOME_SEQUENCING: return DEFAULT_CAPTURE_KIT raise ValueError("No capture kit was found in LIMS") - return target_bed + return target_bed_file def get_germlinecnvcaller_flag(self, analysis_type: str) -> bool: if analysis_type == AnalysisType.WHOLE_GENOME_SEQUENCING: return True return False - def get_workflow_parameters(self, case_id: str) -> RarediseaseParameters: + def get_built_workflow_parameters(self, case_id: str) -> RarediseaseParameters: """Return parameters.""" analysis_type: AnalysisType = self.get_data_analysis_type(case_id=case_id) - target_bed: str = self.get_target_bed(case_id=case_id, analysis_type=analysis_type) + target_bed_file: str = self.get_target_bed(case_id=case_id, analysis_type=analysis_type) skip_germlinecnvcaller = self.get_germlinecnvcaller_flag(analysis_type=analysis_type) outdir = self.get_case_path(case_id=case_id) return RarediseaseParameters( - local_genomes=str(self.references), input=self.get_sample_sheet_path(case_id=case_id), outdir=outdir, analysis_type=analysis_type, - target_bed=Path(self.references, target_bed).as_posix(), + target_bed_file=target_bed_file, save_mapped_as_cram=True, skip_germlinecnvcaller=skip_germlinecnvcaller, vcfanno_extra_resources=f"{outdir}/{ScoutExportFileName.MANAGED_VARIANTS}", @@ -157,10 +156,6 @@ def is_managed_variants_required(self) -> bool: """Return True if a managed variants needs to be exported from Scout.""" return True - @property - def root(self) -> str: - return self.config.raredisease.root - def write_managed_variants(self, case_id: str, content: list[str]) -> None: self._write_managed_variants(out_dir=Path(self.root, case_id), content=content) diff --git a/cg/meta/workflow/rnafusion.py b/cg/meta/workflow/rnafusion.py index 292b4f40bc..14c3da4afe 100644 --- a/cg/meta/workflow/rnafusion.py +++ b/cg/meta/workflow/rnafusion.py @@ -29,14 +29,14 @@ def __init__( ): super().__init__(config=config, workflow=workflow) self.root_dir: str = config.rnafusion.root - self.nfcore_workflow_path: str = config.rnafusion.workflow_path - self.references: str = config.rnafusion.references + self.workflow_bin_path: str = config.rnafusion.workflow_bin_path self.profile: str = config.rnafusion.profile self.conda_env: str = config.rnafusion.conda_env self.conda_binary: str = config.rnafusion.conda_binary - self.config_platform: str = config.rnafusion.config_platform - self.config_params: str = config.rnafusion.config_params - self.config_resources: str = config.rnafusion.config_resources + self.platform: str = config.rnafusion.platform + self.params: str = config.rnafusion.params + self.config: str = config.rnafusion.config + self.resources: str = config.rnafusion.resources self.tower_binary_path: str = config.tower_binary_path self.tower_workflow: str = config.rnafusion.tower_workflow self.account: str = config.rnafusion.slurm.account @@ -50,11 +50,6 @@ def sample_sheet_headers(self) -> list[str]: """Headers for sample sheet.""" return RnafusionSampleSheetEntry.headers() - @property - def is_params_appended_to_nextflow_config(self) -> bool: - """Return True if parameters should be added into the nextflow config file instead of the params file.""" - return False - @property def is_multiple_samples_allowed(self) -> bool: """Return whether the analysis supports multiple samples to be linked to the case.""" @@ -82,21 +77,15 @@ def get_sample_sheet_content_per_sample(self, case_sample: CaseSample) -> list[l ) return sample_sheet_entry.reformat_sample_content() - def get_workflow_parameters( + def get_built_workflow_parameters( self, case_id: str, genomes_base: Path | None = None ) -> RnafusionParameters: """Get Rnafusion parameters.""" return RnafusionParameters( - genomes_base=genomes_base or self.get_references_path(), input=self.get_sample_sheet_path(case_id=case_id), outdir=self.get_case_path(case_id=case_id), ) - def get_references_path(self, genomes_base: Path | None = None) -> Path: - if genomes_base: - return genomes_base.absolute() - return Path(self.references).absolute() - @staticmethod def ensure_mandatory_metrics_present(metrics: list[MetricsBase]) -> None: """Check that all mandatory metrics are present. diff --git a/cg/meta/workflow/taxprofiler.py b/cg/meta/workflow/taxprofiler.py index 9d5f985018..8bcca36c50 100644 --- a/cg/meta/workflow/taxprofiler.py +++ b/cg/meta/workflow/taxprofiler.py @@ -28,7 +28,7 @@ def __init__( ): super().__init__(config=config, workflow=workflow) self.root_dir: str = config.taxprofiler.root - self.nfcore_workflow_path: str = config.taxprofiler.workflow_path + self.workflow_bin_path: str = config.taxprofiler.workflow_bin_path self.conda_env: str = config.taxprofiler.conda_env self.conda_binary: str = config.taxprofiler.conda_binary self.profile: str = config.taxprofiler.profile @@ -47,11 +47,6 @@ def sample_sheet_headers(self) -> list[str]: """Headers for sample sheet.""" return TaxprofilerSampleSheetEntry.headers() - @property - def is_params_appended_to_nextflow_config(self) -> bool: - """Return True if parameters should be added into the nextflow config file instead of the params file.""" - return False - @property def is_multiqc_pattern_search_exact(self) -> bool: """Only exact pattern search is allowed to collect metrics information from multiqc file.""" @@ -82,7 +77,7 @@ def get_sample_sheet_content_per_sample(self, case_sample: CaseSample) -> list[l ) return sample_sheet_entry.reformat_sample_content() - def get_workflow_parameters(self, case_id: str) -> TaxprofilerParameters: + def get_built_workflow_parameters(self, case_id: str) -> TaxprofilerParameters: """Return Taxprofiler parameters.""" return TaxprofilerParameters( cluster_options=f"--qos={self.get_slurm_qos_for_case(case_id=case_id)}", diff --git a/cg/meta/workflow/tomte.py b/cg/meta/workflow/tomte.py index c10c715eea..9eb4aa7df6 100644 --- a/cg/meta/workflow/tomte.py +++ b/cg/meta/workflow/tomte.py @@ -25,14 +25,14 @@ def __init__( ): super().__init__(config=config, workflow=workflow) self.root_dir: str = config.tomte.root - self.nfcore_workflow_path: str = config.tomte.workflow_path - self.references: str = config.tomte.references + self.workflow_bin_path: str = config.tomte.workflow_bin_path self.profile: str = config.tomte.profile self.conda_env: str = config.tomte.conda_env self.conda_binary: str = config.tomte.conda_binary - self.config_platform: str = config.tomte.config_platform - self.config_params: str = config.tomte.config_params - self.config_resources: str = config.tomte.config_resources + self.platform: str = config.tomte.platform + self.params: str = config.tomte.params + self.config: str = config.tomte.config + self.resources: str = config.tomte.resources self.tower_binary_path: str = config.tower_binary_path self.tower_workflow: str = config.tomte.tower_workflow self.account: str = config.tomte.slurm.account @@ -70,7 +70,7 @@ def get_sample_sheet_content_per_sample(self, case_sample: CaseSample) -> list[l ) return sample_sheet_entry.reformat_sample_content - def get_workflow_parameters(self, case_id: str) -> TomteParameters: + def get_built_workflow_parameters(self, case_id: str) -> TomteParameters: """Return parameters.""" return TomteParameters( input=self.get_sample_sheet_path(case_id=case_id), diff --git a/cg/models/cg_config.py b/cg/models/cg_config.py index 3fab14bd92..77af8d2181 100644 --- a/cg/models/cg_config.py +++ b/cg/models/cg_config.py @@ -45,21 +45,13 @@ from cg.services.run_devices.pacbio.housekeeper_service.pacbio_houskeeper_service import ( PacBioHousekeeperService, ) -from cg.services.run_devices.pacbio.metrics_parser.metrics_parser import ( - PacBioMetricsParser, -) -from cg.services.run_devices.pacbio.post_processing_service import ( - PacBioPostProcessingService, -) +from cg.services.run_devices.pacbio.metrics_parser.metrics_parser import PacBioMetricsParser +from cg.services.run_devices.pacbio.post_processing_service import PacBioPostProcessingService from cg.services.run_devices.pacbio.run_data_generator.pacbio_run_data_generator import ( PacBioRunDataGenerator, ) -from cg.services.run_devices.pacbio.run_file_manager.run_file_manager import ( - PacBioRunFileManager, -) -from cg.services.run_devices.pacbio.run_validator.pacbio_run_validator import ( - PacBioRunValidator, -) +from cg.services.run_devices.pacbio.run_file_manager.run_file_manager import PacBioRunFileManager +from cg.services.run_devices.pacbio.run_validator.pacbio_run_validator import PacBioRunValidator from cg.services.run_devices.run_names.pacbio import PacbioRunNamesService from cg.services.sequencing_qc_service.sequencing_qc_service import SequencingQCService from cg.services.slurm_service.slurm_cli_service import SlurmCLIService @@ -223,13 +215,13 @@ class RarediseaseConfig(CommonAppConfig): compute_env: str conda_binary: str | None = None conda_env: str - config_platform: str - config_params: str - config_resources: str + platform: str + params: str + config: str + resources: str launch_directory: str - workflow_path: str + workflow_bin_path: str profile: str - references: str revision: str root: str slurm: SlurmConfig @@ -241,12 +233,12 @@ class TomteConfig(CommonAppConfig): compute_env: str conda_binary: str | None = None conda_env: str - config_platform: str - config_params: str - config_resources: str - workflow_path: str + platform: str + params: str + config: str + resources: str + workflow_bin_path: str profile: str - references: str revision: str root: str slurm: SlurmConfig @@ -258,17 +250,17 @@ class RnafusionConfig(CommonAppConfig): compute_env: str conda_binary: str | None = None conda_env: str - config_platform: str - config_params: str - config_resources: str + platform: str + params: str + config: str + resources: str launch_directory: str profile: str - references: str revision: str root: str slurm: SlurmConfig tower_workflow: str - workflow_path: str + workflow_bin_path: str class TaxprofilerConfig(CommonAppConfig): @@ -278,7 +270,7 @@ class TaxprofilerConfig(CommonAppConfig): compute_env: str databases: str hostremoval_reference: str - workflow_path: str + workflow_bin_path: str profile: str revision: str root: str diff --git a/cg/models/raredisease/raredisease.py b/cg/models/raredisease/raredisease.py index 764b4b4056..1797d6c78c 100644 --- a/cg/models/raredisease/raredisease.py +++ b/cg/models/raredisease/raredisease.py @@ -64,10 +64,9 @@ def list(cls) -> list[str]: class RarediseaseParameters(WorkflowParameters): """Model for Raredisease parameters.""" - target_bed: str + target_bed_file: str analysis_type: str save_mapped_as_cram: bool skip_germlinecnvcaller: bool vcfanno_extra_resources: str - local_genomes: str vep_filters_scout_fmt: str diff --git a/cg/models/rnafusion/rnafusion.py b/cg/models/rnafusion/rnafusion.py index 6c4b1dcd1c..f8bd497677 100644 --- a/cg/models/rnafusion/rnafusion.py +++ b/cg/models/rnafusion/rnafusion.py @@ -1,5 +1,3 @@ -from pathlib import Path - from cg.constants.constants import Strandedness from cg.models.nf_analysis import NextflowSampleSheetEntry, WorkflowParameters from cg.models.qc_metrics import QCMetrics @@ -26,8 +24,6 @@ class RnafusionQCMetrics(QCMetrics): class RnafusionParameters(WorkflowParameters): """Rnafusion parameters.""" - genomes_base: Path - class RnafusionSampleSheetEntry(NextflowSampleSheetEntry): """Rnafusion sample sheet model.""" diff --git a/tests/cli/generate/delivery_report/test_cli.py b/tests/cli/generate/delivery_report/test_cli.py index fa85a23c48..a98d83ea05 100644 --- a/tests/cli/generate/delivery_report/test_cli.py +++ b/tests/cli/generate/delivery_report/test_cli.py @@ -2,10 +2,10 @@ import pytest from _pytest.fixtures import FixtureRequest -from click.testing import Result, CliRunner +from click.testing import CliRunner, Result from cg.cli.generate.delivery_report.base import generate_delivery_report -from cg.constants import Workflow, EXIT_SUCCESS +from cg.constants import EXIT_SUCCESS, Workflow from cg.models.cg_config import CGConfig @@ -33,7 +33,7 @@ def test_generate_delivery_report( @pytest.mark.parametrize("workflow", [Workflow.RAREDISEASE, Workflow.RNAFUSION]) -def test_generate_delivery_report_dry_rum( +def test_generate_delivery_report_dry_run( request: FixtureRequest, workflow: Workflow, cli_runner: CliRunner ) -> None: """Test dry run command to generate delivery report.""" diff --git a/tests/conftest.py b/tests/conftest.py index b3c313e98e..03206c4271 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1900,6 +1900,7 @@ def context_config( nextflow_binary: Path, nf_analysis_platform_config_path: Path, nf_analysis_pipeline_params_path: Path, + nf_analysis_pipeline_config_path: Path, nf_analysis_pipeline_resource_optimisation_path: Path, ) -> dict: """Return a context config.""" @@ -2085,11 +2086,12 @@ def context_config( "compute_env": "nf_tower_compute_env", "conda_binary": conda_binary.as_posix(), "conda_env": "S_raredisease", - "config_platform": str(nf_analysis_platform_config_path), - "config_params": str(nf_analysis_pipeline_params_path), - "config_resources": str(nf_analysis_pipeline_resource_optimisation_path), + "platform": str(nf_analysis_platform_config_path), + "params": str(nf_analysis_pipeline_params_path), + "config": str(nf_analysis_pipeline_config_path), + "resources": str(nf_analysis_pipeline_resource_optimisation_path), "launch_directory": Path("path", "to", "launchdir").as_posix(), - "workflow_path": Path("workflow", "path").as_posix(), + "workflow_bin_path": Path("workflow", "path").as_posix(), "profile": "myprofile", "references": Path("path", "to", "references").as_posix(), "revision": "2.2.0", @@ -2105,10 +2107,11 @@ def context_config( "compute_env": "nf_tower_compute_env", "conda_binary": conda_binary.as_posix(), "conda_env": "S_tomte", - "config_platform": str(nf_analysis_platform_config_path), - "config_params": str(nf_analysis_pipeline_params_path), - "config_resources": str(nf_analysis_pipeline_resource_optimisation_path), - "workflow_path": Path("workflow", "path").as_posix(), + "platform": str(nf_analysis_platform_config_path), + "params": str(nf_analysis_pipeline_params_path), + "config": str(nf_analysis_pipeline_config_path), + "resources": str(nf_analysis_pipeline_resource_optimisation_path), + "workflow_bin_path": Path("workflow", "path").as_posix(), "profile": "myprofile", "references": Path("path", "to", "references").as_posix(), "revision": "2.2.0", @@ -2124,11 +2127,12 @@ def context_config( "compute_env": "nf_tower_compute_env", "conda_binary": conda_binary.as_posix(), "conda_env": "S_RNAFUSION", - "config_platform": str(nf_analysis_platform_config_path), - "config_params": str(nf_analysis_pipeline_params_path), - "config_resources": str(nf_analysis_pipeline_resource_optimisation_path), + "platform": str(nf_analysis_platform_config_path), + "params": str(nf_analysis_pipeline_params_path), + "config": str(nf_analysis_pipeline_config_path), + "resources": str(nf_analysis_pipeline_resource_optimisation_path), "launch_directory": Path("path", "to", "launchdir").as_posix(), - "workflow_path": Path("workflow", "path").as_posix(), + "workflow_bin_path": Path("workflow", "path").as_posix(), "profile": "myprofile", "references": Path("path", "to", "references").as_posix(), "revision": "2.2.0", @@ -2148,7 +2152,7 @@ def context_config( "conda_binary": conda_binary.as_posix(), "conda_env": "S_taxprofiler", "launch_directory": Path("path", "to", "launchdir").as_posix(), - "workflow_path": Path("workflow", "path").as_posix(), + "workflow_bin_path": Path("workflow", "path").as_posix(), "databases": Path("path", "to", "databases").as_posix(), "profile": "myprofile", "hostremoval_reference": Path("path", "to", "hostremoval_reference").as_posix(), @@ -2575,14 +2579,13 @@ def raredisease_parameters_default( return RarediseaseParameters( input=raredisease_sample_sheet_path, outdir=Path(raredisease_dir, raredisease_case_id), - target_bed=bed_version_file_name, + target_bed_file=bed_version_file_name, skip_germlinecnvcaller=False, analysis_type=AnalysisTypes.WES, save_mapped_as_cram=True, vcfanno_extra_resources=str( Path(raredisease_dir, raredisease_case_id + ScoutExportFileName.MANAGED_VARIANTS) ), - local_genomes=Path(raredisease_dir, "references").as_posix(), vep_filters_scout_fmt=str( Path(raredisease_dir, raredisease_case_id + ScoutExportFileName.PANELS) ), @@ -3022,7 +3025,13 @@ def nf_analysis_platform_config_path(nf_analysis_analysis_dir) -> Path: @pytest.fixture(scope="function") def nf_analysis_pipeline_params_path(nf_analysis_analysis_dir) -> Path: """Path to pipeline params file.""" - return Path(nf_analysis_analysis_dir, "pipeline_params").with_suffix(FileExtensions.CONFIG) + return Path(nf_analysis_analysis_dir, "pipeline_params").with_suffix(FileExtensions.YAML) + + +@pytest.fixture(scope="function") +def nf_analysis_pipeline_config_path(nf_analysis_analysis_dir) -> Path: + """Path to pipeline params file.""" + return Path(nf_analysis_analysis_dir, "pipeline_config").with_suffix(FileExtensions.CONFIG) @pytest.fixture(scope="function") diff --git a/tests/fixtures/analysis/nf-analysis/pipeline_params.config b/tests/fixtures/analysis/nf-analysis/pipeline_config.config similarity index 100% rename from tests/fixtures/analysis/nf-analysis/pipeline_params.config rename to tests/fixtures/analysis/nf-analysis/pipeline_config.config diff --git a/tests/fixtures/analysis/nf-analysis/pipeline_params.yaml b/tests/fixtures/analysis/nf-analysis/pipeline_params.yaml new file mode 100644 index 0000000000..a27226f432 --- /dev/null +++ b/tests/fixtures/analysis/nf-analysis/pipeline_params.yaml @@ -0,0 +1 @@ +someparam: "something" diff --git a/tests/io/test_io_config.py b/tests/io/test_io_config.py deleted file mode 100644 index 7521cbe084..0000000000 --- a/tests/io/test_io_config.py +++ /dev/null @@ -1,17 +0,0 @@ -from pathlib import Path - -import pytest - -from cg.io.config import write_config_nextflow_style - - -def test_write_config_nextflow_style(config_dict: Path): - """ - Test output content from stream into nextflow config format. - """ - - # THEN assert a config format is returned - assert ( - write_config_nextflow_style(content=config_dict) - == 'params.input = "input_path"\nparams.output = "output_path"\n' - ) From 9037ced3481d7b08ad67b293cc5c48f882305a14 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Wed, 27 Nov 2024 11:54:28 +0000 Subject: [PATCH 04/25] =?UTF-8?q?Bump=20version:=2064.5.13=20=E2=86=92=206?= =?UTF-8?q?4.5.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 f23b078481..61f5e2b513 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 64.5.13 +current_version = 64.5.14 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index e33450639f..c7ef26cad4 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "64.5.13" +__version__ = "64.5.14" diff --git a/pyproject.toml b/pyproject.toml index 988b37df4b..2310eed895 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "cg" -version = "64.5.13" +version = "64.5.14" description = "Clinical Genomics command center" authors = ["Clinical Genomics "] readme = "README.md" From 81a36609a53342b393fcb335f253956f60ca8228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20Ohlsson=20=C3=85ngnell?= <40887124+islean@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:59:28 +0100 Subject: [PATCH 05/25] (Improve order flow) Update test endpoint (#3978) (patch) ### Fixed - All available order types are configured in validate_order --- cg/server/endpoints/orders.py | 25 +++++++++++++------ cg/server/ext.py | 15 ++++++++++- .../order_validation_service/models/case.py | 5 ++++ .../workflows/mip_dna/models/case.py | 10 +++++--- .../workflows/tomte/models/case.py | 5 ---- 5 files changed, 43 insertions(+), 17 deletions(-) diff --git a/cg/server/endpoints/orders.py b/cg/server/endpoints/orders.py index faf43787ee..34f375cea9 100644 --- a/cg/server/endpoints/orders.py +++ b/cg/server/endpoints/orders.py @@ -14,7 +14,7 @@ from cg.apps.orderform.excel_orderform_parser import ExcelOrderformParser from cg.apps.orderform.json_orderform_parser import JsonOrderformParser from cg.constants import ANALYSIS_SOURCES, METAGENOME_SOURCES -from cg.constants.constants import FileFormat, Workflow +from cg.constants.constants import FileFormat from cg.exc import ( OrderError, OrderExistsError, @@ -34,11 +34,14 @@ from cg.server.dto.orders.orders_response import Order, OrdersResponse from cg.server.endpoints.utils import before_request from cg.server.ext import ( + balsamic_validation_service, db, delivery_message_service, lims, + microbial_fastq_validation_service, microsalt_validation_service, mip_dna_validation_service, + mutant_validation_service, order_service, order_submitter_registry, rna_fusion_validation_service, @@ -263,18 +266,24 @@ def get_options(): ) -@ORDERS_BLUEPRINT.route("/validate_order/", methods=["POST"]) -def validate_order(workflow: str): +@ORDERS_BLUEPRINT.route("/validate_order/", methods=["POST"]) +def validate_order(order_type: OrderType): raw_order = request.get_json() - raw_order["workflow"] = workflow + raw_order["workflow"] = order_type raw_order["user_id"] = g.current_user.id response = {} - if workflow == Workflow.MICROSALT: + if order_type == OrderType.BALSAMIC: + response = balsamic_validation_service.validate(raw_order) + if order_type == OrderType.MICROBIAL_FASTQ: + response = microbial_fastq_validation_service.validate(raw_order) + if order_type == OrderType.MICROSALT: response = microsalt_validation_service.validate(raw_order) - if workflow == Workflow.MIP_DNA: + if order_type == OrderType.MIP_DNA: response = mip_dna_validation_service.validate(raw_order) - if workflow == Workflow.RNAFUSION: + if order_type == OrderType.SARS_COV_2: + response = mutant_validation_service.validate(raw_order) + if order_type == OrderType.RNAFUSION: response = rna_fusion_validation_service.validate(raw_order) - if workflow == Workflow.TOMTE: + if order_type == OrderType.TOMTE: response = tomte_validation_service.validate(raw_order) return jsonify(response), HTTPStatus.OK diff --git a/cg/server/ext.py b/cg/server/ext.py index 678fffb155..ff2ed1ad84 100644 --- a/cg/server/ext.py +++ b/cg/server/ext.py @@ -12,12 +12,21 @@ from cg.server.app_config import app_config from cg.services.application.service import ApplicationsWebService from cg.services.delivery_message.delivery_message_service import DeliveryMessageService +from cg.services.order_validation_service.workflows.balsamic.validation_service import ( + BalsamicValidationService, +) +from cg.services.order_validation_service.workflows.microbial_fastq.validation_service import ( + MicrobialFastqValidationService, +) from cg.services.order_validation_service.workflows.microsalt.validation_service import ( MicroSaltValidationService, ) from cg.services.order_validation_service.workflows.mip_dna.validation_service import ( MipDnaValidationService, ) +from cg.services.order_validation_service.workflows.mutant.validation_service import ( + MutantValidationService, +) from cg.services.order_validation_service.workflows.rna_fusion.validation_service import ( RnaFusionValidationService, ) @@ -109,10 +118,14 @@ def init_app(self, app): status_db=db, ) -tomte_validation_service = TomteValidationService(store=db) +balsamic_validation_service = BalsamicValidationService(store=db) +microbial_fastq_validation_service = MicrobialFastqValidationService(store=db) microsalt_validation_service = MicroSaltValidationService(store=db) mip_dna_validation_service = MipDnaValidationService(store=db) +mutant_validation_service = MutantValidationService(store=db) rna_fusion_validation_service = RnaFusionValidationService(store=db) +tomte_validation_service = TomteValidationService(store=db) + freshdesk_client = FreshdeskClient( base_url=app_config.freshdesk_url, api_key=app_config.freshdesk_api_key ) diff --git a/cg/services/order_validation_service/models/case.py b/cg/services/order_validation_service/models/case.py index 861c56204d..8ecdaec555 100644 --- a/cg/services/order_validation_service/models/case.py +++ b/cg/services/order_validation_service/models/case.py @@ -45,6 +45,11 @@ def enumerated_existing_samples(self) -> list[tuple[int, ExistingSample]]: samples.append((sample_index, sample)) return samples + def get_sample(self, sample_name: str) -> Sample | None: + for sample in self.samples: + if sample.name == sample_name: + return sample + @model_validator(mode="before") def convert_empty_strings_to_none(cls, data): if isinstance(data, dict): diff --git a/cg/services/order_validation_service/workflows/mip_dna/models/case.py b/cg/services/order_validation_service/workflows/mip_dna/models/case.py index 924f75964e..28fddcf4b3 100644 --- a/cg/services/order_validation_service/workflows/mip_dna/models/case.py +++ b/cg/services/order_validation_service/workflows/mip_dna/models/case.py @@ -4,9 +4,7 @@ from cg.services.order_validation_service.models.case import Case from cg.services.order_validation_service.models.discriminators import has_internal_id from cg.services.order_validation_service.models.existing_sample import ExistingSample -from cg.services.order_validation_service.workflows.mip_dna.models.sample import ( - MipDnaSample, -) +from cg.services.order_validation_service.workflows.mip_dna.models.sample import MipDnaSample NewSample = Annotated[MipDnaSample, Tag("new")] OldSample = Annotated[ExistingSample, Tag("existing")] @@ -17,3 +15,9 @@ class MipDnaCase(Case): panels: list[str] synopsis: str | None = None samples: list[Annotated[NewSample | OldSample, Discriminator(has_internal_id)]] + + def get_samples_with_father(self) -> list[tuple[MipDnaSample, int]]: + return [(sample, index) for index, sample in self.enumerated_samples if sample.father] + + def get_samples_with_mother(self) -> list[tuple[MipDnaSample, int]]: + return [(sample, index) for index, sample in self.enumerated_samples if sample.mother] diff --git a/cg/services/order_validation_service/workflows/tomte/models/case.py b/cg/services/order_validation_service/workflows/tomte/models/case.py index 38a981fc25..1e78e140b0 100644 --- a/cg/services/order_validation_service/workflows/tomte/models/case.py +++ b/cg/services/order_validation_service/workflows/tomte/models/case.py @@ -16,11 +16,6 @@ class TomteCase(Case): synopsis: str | None = None samples: list[Annotated[NewSample | OldSample, Discriminator(has_internal_id)]] - def get_sample(self, sample_name: str) -> TomteSample | None: - for sample in self.samples: - if sample.name == sample_name: - return sample - def get_samples_with_father(self) -> list[tuple[TomteSample, int]]: return [(sample, index) for index, sample in self.enumerated_samples if sample.father] From 2b2f36251c3733c4004df4e25a1ae9c74bfba3fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20Ohlsson=20=C3=85ngnell?= <40887124+islean@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:33:59 +0100 Subject: [PATCH 06/25] Patch orders pagination (#3981) (patch) ### Fixed - Pagination is applied even when workflow is not set --- cg/store/crud/read.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cg/store/crud/read.py b/cg/store/crud/read.py index 209d0f35d2..e567019259 100644 --- a/cg/store/crud/read.py +++ b/cg/store/crud/read.py @@ -1418,7 +1418,8 @@ def get_orders(self, orders_params: OrderQueryParams) -> tuple[list[Order], int] cases=orders, filter_functions=[CaseFilter.BY_WORKFLOWS], workflows=orders_params.workflows, - ).distinct() + ) + orders = orders.distinct() orders: Query = apply_order_filters( orders=orders, filters=[OrderFilter.BY_SEARCH, OrderFilter.BY_OPEN], From c1213cdeb1f304db8005c8eda36a86f4408f11c4 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Wed, 27 Nov 2024 15:34:27 +0000 Subject: [PATCH 07/25] =?UTF-8?q?Bump=20version:=2064.5.14=20=E2=86=92=206?= =?UTF-8?q?4.5.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 61f5e2b513..29aa6aeade 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 64.5.14 +current_version = 64.5.15 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index c7ef26cad4..1285ee286e 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "64.5.14" +__version__ = "64.5.15" diff --git a/pyproject.toml b/pyproject.toml index 2310eed895..54d54c2e6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "cg" -version = "64.5.14" +version = "64.5.15" description = "Clinical Genomics command center" authors = ["Clinical Genomics "] readme = "README.md" From 077cf68e02ac3b7711f0456b39dc5902c93a3099 Mon Sep 17 00:00:00 2001 From: Henrik Stranneheim Date: Thu, 28 Nov 2024 09:14:50 +0100 Subject: [PATCH 08/25] feat(analysisType): Refactor 2 (#3977) ### Change - Refactor AnalysisType to use SeqLibraryPrepCategory to keep it DRY --- cg/constants/tb.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cg/constants/tb.py b/cg/constants/tb.py index c52267f818..bf8768244d 100644 --- a/cg/constants/tb.py +++ b/cg/constants/tb.py @@ -1,5 +1,7 @@ from enum import StrEnum +from cg.constants.sequencing import SeqLibraryPrepCategory + class AnalysisStatus: CANCELLED: str = "cancelled" @@ -13,8 +15,8 @@ class AnalysisStatus: class AnalysisTypes(StrEnum): - WGS: str = "wgs" - WES: str = "wes" - TGS: str = "tgs" - WTS: str = "wts" OTHER: str = "other" + TGS: str = SeqLibraryPrepCategory.TARGETED_GENOME_SEQUENCING + WES: str = SeqLibraryPrepCategory.WHOLE_EXOME_SEQUENCING + WGS: str = SeqLibraryPrepCategory.WHOLE_GENOME_SEQUENCING + WTS: str = SeqLibraryPrepCategory.WHOLE_TRANSCRIPTOME_SEQUENCING From a0b486dbac100983751bc98ff39a211fb0575d67 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Thu, 28 Nov 2024 08:15:16 +0000 Subject: [PATCH 09/25] =?UTF-8?q?Bump=20version:=2064.5.15=20=E2=86=92=206?= =?UTF-8?q?4.5.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 29aa6aeade..c9a5308898 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 64.5.15 +current_version = 64.5.16 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index 1285ee286e..38fb70b985 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "64.5.15" +__version__ = "64.5.16" diff --git a/pyproject.toml b/pyproject.toml index 54d54c2e6e..b35e610173 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "cg" -version = "64.5.15" +version = "64.5.16" description = "Clinical Genomics command center" authors = ["Clinical Genomics "] readme = "README.md" From cfb73faa97918beec9f7ab58353faa9239bba78b Mon Sep 17 00:00:00 2001 From: ChristianOertlin Date: Thu, 28 Nov 2024 15:32:21 +0100 Subject: [PATCH 10/25] add( rich click to cg) (#3975) (patch) # Description add rich click --- cg/apps/demultiplex/sample_sheet/api.py | 2 +- cg/cli/add.py | 2 +- cg/cli/archive.py | 2 +- cg/cli/backup.py | 2 +- cg/cli/base.py | 2 +- cg/cli/clean.py | 2 +- cg/cli/compress/base.py | 2 +- cg/cli/compress/fastq.py | 2 +- cg/cli/delete/base.py | 2 +- cg/cli/delete/case.py | 2 +- cg/cli/delete/cases.py | 2 +- cg/cli/delete/illumina_sequencing_run.py | 2 +- cg/cli/delete/observations.py | 2 +- cg/cli/deliver/base.py | 2 +- cg/cli/demultiplex/base.py | 2 +- cg/cli/demultiplex/demux.py | 2 +- cg/cli/demultiplex/finish.py | 2 +- cg/cli/demultiplex/sample_sheet.py | 2 +- cg/cli/downsample.py | 2 +- cg/cli/generate/base.py | 2 +- cg/cli/generate/delivery_report/base.py | 2 +- cg/cli/generate/delivery_report/options.py | 2 +- cg/cli/generate/delivery_report/utils.py | 2 +- cg/cli/get.py | 2 +- cg/cli/post_process/post_process.py | 2 +- cg/cli/sequencing_qc/sequencing_qc.py | 2 +- cg/cli/set/base.py | 2 +- cg/cli/set/case.py | 2 +- cg/cli/set/cases.py | 2 +- cg/cli/store/base.py | 2 +- cg/cli/store/store.py | 2 +- cg/cli/transfer.py | 2 +- cg/cli/upload/base.py | 2 +- cg/cli/upload/coverage.py | 2 +- cg/cli/upload/delivery_report.py | 2 +- cg/cli/upload/fohm.py | 2 +- cg/cli/upload/genotype.py | 2 +- cg/cli/upload/gens.py | 2 +- cg/cli/upload/gisaid.py | 2 +- cg/cli/upload/mutacc.py | 2 +- cg/cli/upload/nipt/base.py | 2 +- cg/cli/upload/nipt/ftp.py | 2 +- cg/cli/upload/nipt/statina.py | 2 +- cg/cli/upload/observations/observations.py | 2 +- cg/cli/upload/scout.py | 2 +- cg/cli/upload/utils.py | 2 +- cg/cli/upload/validate.py | 2 +- cg/cli/utils.py | 2 +- cg/cli/workflow/balsamic/base.py | 2 +- cg/cli/workflow/balsamic/options.py | 2 +- cg/cli/workflow/balsamic/pon.py | 2 +- cg/cli/workflow/balsamic/qc.py | 2 +- cg/cli/workflow/balsamic/umi.py | 2 +- cg/cli/workflow/base.py | 2 +- cg/cli/workflow/commands.py | 2 +- cg/cli/workflow/fluffy/base.py | 2 +- cg/cli/workflow/jasen/base.py | 2 +- cg/cli/workflow/microsalt/base.py | 2 +- cg/cli/workflow/mip/base.py | 2 +- cg/cli/workflow/mip/options.py | 2 +- cg/cli/workflow/mip_dna/base.py | 2 +- cg/cli/workflow/mip_rna/base.py | 2 +- cg/cli/workflow/mutant/base.py | 2 +- cg/cli/workflow/nf_analysis.py | 2 +- cg/cli/workflow/raredisease/base.py | 2 +- cg/cli/workflow/raw_data/base.py | 2 +- cg/cli/workflow/rnafusion/base.py | 2 +- cg/cli/workflow/taxprofiler/base.py | 2 +- cg/cli/workflow/tomte/base.py | 2 +- cg/cli/workflow/utils.py | 2 +- cg/constants/cli_options.py | 2 +- cg/meta/archive/archive.py | 2 +- cg/meta/upload/balsamic/balsamic.py | 2 +- .../upload/microsalt/microsalt_upload_api.py | 2 +- cg/meta/upload/mip/mip_dna.py | 2 +- cg/meta/upload/mip/mip_rna.py | 2 +- cg/meta/upload/nf_analysis.py | 2 +- cg/meta/upload/raredisease/raredisease.py | 2 +- cg/meta/upload/tomte/tomte.py | 2 +- cg/meta/upload/upload_api.py | 2 +- cg/meta/workflow/analysis.py | 2 +- cg/meta/workflow/microsalt/microsalt.py | 2 +- cg/utils/click/EnumChoice.py | 2 +- poetry.lock | 24 +++++++++++++++++-- pyproject.toml | 1 + .../demultiplex/test_validate_sample_sheet.py | 4 +--- .../context_fixtures.py | 2 +- 87 files changed, 108 insertions(+), 89 deletions(-) diff --git a/cg/apps/demultiplex/sample_sheet/api.py b/cg/apps/demultiplex/sample_sheet/api.py index 353587f450..c581c0de0c 100644 --- a/cg/apps/demultiplex/sample_sheet/api.py +++ b/cg/apps/demultiplex/sample_sheet/api.py @@ -1,7 +1,7 @@ import logging from pathlib import Path -import click +import rich_click as click from cg.apps.demultiplex.sample_sheet.read_sample_sheet import get_samples_from_content from cg.apps.demultiplex.sample_sheet.sample_models import IlluminaSampleIndexSetting diff --git a/cg/cli/add.py b/cg/cli/add.py index 3aae8edbac..be9bb5b961 100644 --- a/cg/cli/add.py +++ b/cg/cli/add.py @@ -1,6 +1,6 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS, is_case_name_allowed from cg.constants import DataDelivery, Priority, Workflow diff --git a/cg/cli/archive.py b/cg/cli/archive.py index 35a76a3e9b..08ca079a78 100644 --- a/cg/cli/archive.py +++ b/cg/cli/archive.py @@ -1,4 +1,4 @@ -import click +import rich_click as click from click.core import ParameterSource from cg.cli.utils import CLICK_CONTEXT_SETTINGS diff --git a/cg/cli/backup.py b/cg/cli/backup.py index 7b18cdb127..b59b618acd 100644 --- a/cg/cli/backup.py +++ b/cg/cli/backup.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Iterable -import click +import rich_click as click import housekeeper.store.models as hk_models from cg.apps.housekeeper.hk import HousekeeperAPI diff --git a/cg/cli/base.py b/cg/cli/base.py index 761644d3bb..9ba7925f52 100644 --- a/cg/cli/base.py +++ b/cg/cli/base.py @@ -4,7 +4,7 @@ import sys from pathlib import Path -import click +import rich_click as click import coloredlogs from sqlalchemy.orm import scoped_session diff --git a/cg/cli/clean.py b/cg/cli/clean.py index 384c6f5fed..5dff4f00d2 100644 --- a/cg/cli/clean.py +++ b/cg/cli/clean.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta from pathlib import Path -import click +import rich_click as click from housekeeper.store.models import File, Version from cg.apps.housekeeper.hk import HousekeeperAPI diff --git a/cg/cli/compress/base.py b/cg/cli/compress/base.py index 07094e925d..c64ee62d7c 100644 --- a/cg/cli/compress/base.py +++ b/cg/cli/compress/base.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.compress.fastq import ( clean_fastq, diff --git a/cg/cli/compress/fastq.py b/cg/cli/compress/fastq.py index 7ce779c8e8..156cb3ae77 100644 --- a/cg/cli/compress/fastq.py +++ b/cg/cli/compress/fastq.py @@ -3,7 +3,7 @@ import logging from typing import Iterable -import click +import rich_click as click from cg.apps.housekeeper.hk import HousekeeperAPI from cg.cli.compress.helpers import ( diff --git a/cg/cli/delete/base.py b/cg/cli/delete/base.py index 456e51d8b8..35d9b797da 100644 --- a/cg/cli/delete/base.py +++ b/cg/cli/delete/base.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.delete.case import delete_case from cg.cli.delete.cases import delete_cases diff --git a/cg/cli/delete/case.py b/cg/cli/delete/case.py index 54e6c88940..775c70aaf8 100644 --- a/cg/cli/delete/case.py +++ b/cg/cli/delete/case.py @@ -3,7 +3,7 @@ import datetime import logging -import click +import rich_click as click from cg.cli.get import get_case as print_case from cg.constants.cli_options import DRY_RUN, SKIP_CONFIRMATION diff --git a/cg/cli/delete/cases.py b/cg/cli/delete/cases.py index bcc9f2654b..e39373fe42 100644 --- a/cg/cli/delete/cases.py +++ b/cg/cli/delete/cases.py @@ -1,6 +1,6 @@ import logging -import click +import rich_click as click from cg.cli.delete.case import delete_case from cg.constants.cli_options import DRY_RUN diff --git a/cg/cli/delete/illumina_sequencing_run.py b/cg/cli/delete/illumina_sequencing_run.py index 8043582a35..0a0c70801e 100644 --- a/cg/cli/delete/illumina_sequencing_run.py +++ b/cg/cli/delete/illumina_sequencing_run.py @@ -1,4 +1,4 @@ -import click +import rich_click as click from cg.constants.cli_options import DRY_RUN from cg.services.illumina.post_processing.housekeeper_storage import ( diff --git a/cg/cli/delete/observations.py b/cg/cli/delete/observations.py index 32db5fa4cf..1ce8566314 100644 --- a/cg/cli/delete/observations.py +++ b/cg/cli/delete/observations.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from sqlalchemy.orm import Query from cg.cli.upload.observations.utils import get_observations_api diff --git a/cg/cli/deliver/base.py b/cg/cli/deliver/base.py index 39ba54e19e..265fba2f8f 100644 --- a/cg/cli/deliver/base.py +++ b/cg/cli/deliver/base.py @@ -3,7 +3,7 @@ import logging from pathlib import Path -import click +import rich_click as click from cg.apps.tb import TrailblazerAPI from cg.cli.deliver.utils import deliver_raw_data_for_analyses diff --git a/cg/cli/demultiplex/base.py b/cg/cli/demultiplex/base.py index 4954b8b27b..a6baa60669 100644 --- a/cg/cli/demultiplex/base.py +++ b/cg/cli/demultiplex/base.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.demultiplex.demux import ( diff --git a/cg/cli/demultiplex/demux.py b/cg/cli/demultiplex/demux.py index 4bc41f1103..187ead1222 100644 --- a/cg/cli/demultiplex/demux.py +++ b/cg/cli/demultiplex/demux.py @@ -2,7 +2,7 @@ from glob import glob from pathlib import Path -import click +import rich_click as click from pydantic import ValidationError from cg.apps.demultiplex.demultiplex_api import DemultiplexingAPI diff --git a/cg/cli/demultiplex/finish.py b/cg/cli/demultiplex/finish.py index db710ce96d..0328f24c25 100644 --- a/cg/cli/demultiplex/finish.py +++ b/cg/cli/demultiplex/finish.py @@ -3,7 +3,7 @@ import logging from pathlib import Path -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.constants.cli_options import DRY_RUN, FORCE diff --git a/cg/cli/demultiplex/sample_sheet.py b/cg/cli/demultiplex/sample_sheet.py index 3a860601df..42be67952c 100644 --- a/cg/cli/demultiplex/sample_sheet.py +++ b/cg/cli/demultiplex/sample_sheet.py @@ -1,7 +1,7 @@ import logging from pathlib import Path -import click +import rich_click as click from pydantic import ValidationError from cg.apps.demultiplex.sample_sheet.api import IlluminaSampleSheetService diff --git a/cg/cli/downsample.py b/cg/cli/downsample.py index bd24e94a33..2b1695ee45 100644 --- a/cg/cli/downsample.py +++ b/cg/cli/downsample.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Tuple -import click +import rich_click as click from cg.apps.downsample.downsample import DownsampleAPI from cg.apps.downsample.utils import store_downsampled_sample_bundle diff --git a/cg/cli/generate/base.py b/cg/cli/generate/base.py index c1f32e0efd..67de232244 100644 --- a/cg/cli/generate/base.py +++ b/cg/cli/generate/base.py @@ -1,6 +1,6 @@ """Common CLI file generation functions""" -import click +import rich_click as click from cg.cli.generate.delivery_report.base import ( generate_available_delivery_reports, diff --git a/cg/cli/generate/delivery_report/base.py b/cg/cli/generate/delivery_report/base.py index 834c4900b8..ab1676f985 100644 --- a/cg/cli/generate/delivery_report/base.py +++ b/cg/cli/generate/delivery_report/base.py @@ -5,7 +5,7 @@ from datetime import datetime from pathlib import Path -import click +import rich_click as click from housekeeper.store.models import Version from cg.cli.generate.delivery_report.options import ( diff --git a/cg/cli/generate/delivery_report/options.py b/cg/cli/generate/delivery_report/options.py index f41af3a4eb..54ac10aac7 100644 --- a/cg/cli/generate/delivery_report/options.py +++ b/cg/cli/generate/delivery_report/options.py @@ -1,6 +1,6 @@ """Delivery report specific command options.""" -import click +import rich_click as click from cg.constants import REPORT_SUPPORTED_WORKFLOW diff --git a/cg/cli/generate/delivery_report/utils.py b/cg/cli/generate/delivery_report/utils.py index 66ab9ad6c7..fc11f4e321 100644 --- a/cg/cli/generate/delivery_report/utils.py +++ b/cg/cli/generate/delivery_report/utils.py @@ -3,7 +3,7 @@ import logging from datetime import datetime -import click +import rich_click as click from cg.constants import REPORT_SUPPORTED_DATA_DELIVERY, REPORT_SUPPORTED_WORKFLOW, Workflow from cg.meta.delivery_report.balsamic import BalsamicDeliveryReportAPI diff --git a/cg/cli/get.py b/cg/cli/get.py index bf73a29121..ea49e4d5fd 100644 --- a/cg/cli/get.py +++ b/cg/cli/get.py @@ -2,7 +2,7 @@ import re from typing import Iterable -import click +import rich_click as click from tabulate import tabulate from cg.cli.utils import CLICK_CONTEXT_SETTINGS diff --git a/cg/cli/post_process/post_process.py b/cg/cli/post_process/post_process.py index f0fc1ccd8f..861d42c853 100644 --- a/cg/cli/post_process/post_process.py +++ b/cg/cli/post_process/post_process.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.post_process.utils import ( UnprocessedRunInfo, diff --git a/cg/cli/sequencing_qc/sequencing_qc.py b/cg/cli/sequencing_qc/sequencing_qc.py index 031c25fa2e..c1b20e6681 100644 --- a/cg/cli/sequencing_qc/sequencing_qc.py +++ b/cg/cli/sequencing_qc/sequencing_qc.py @@ -1,5 +1,5 @@ import logging -import click +import rich_click as click from cg.models.cg_config import CGConfig from cg.services.sequencing_qc_service.sequencing_qc_service import SequencingQCService diff --git a/cg/cli/set/base.py b/cg/cli/set/base.py index 08a9fc9034..d06aad0522 100644 --- a/cg/cli/set/base.py +++ b/cg/cli/set/base.py @@ -5,7 +5,7 @@ import logging from typing import Iterable -import click +import rich_click as click from cg.cli.set.case import set_case from cg.cli.set.cases import set_cases diff --git a/cg/cli/set/case.py b/cg/cli/set/case.py index e84c8af929..40c1df7c18 100644 --- a/cg/cli/set/case.py +++ b/cg/cli/set/case.py @@ -3,7 +3,7 @@ import logging from typing import Callable -import click +import rich_click as click from cg.constants import DataDelivery, Priority, Workflow from cg.constants.constants import CaseActions diff --git a/cg/cli/set/cases.py b/cg/cli/set/cases.py index 1c4b81433a..243d5f5020 100644 --- a/cg/cli/set/cases.py +++ b/cg/cli/set/cases.py @@ -1,6 +1,6 @@ import logging -import click +import rich_click as click from cg.cli.set.case import set_case from cg.constants import Priority diff --git a/cg/cli/store/base.py b/cg/cli/store/base.py index a9319605b2..8ef8520f41 100644 --- a/cg/cli/store/base.py +++ b/cg/cli/store/base.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.apps.crunchy.crunchy import CrunchyAPI from cg.apps.housekeeper.hk import HousekeeperAPI diff --git a/cg/cli/store/store.py b/cg/cli/store/store.py index 95513d8267..0f54f18045 100644 --- a/cg/cli/store/store.py +++ b/cg/cli/store/store.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Iterable -import click +import rich_click as click from housekeeper.store.models import File from cg.apps.crunchy.files import update_metadata_paths diff --git a/cg/cli/transfer.py b/cg/cli/transfer.py index 720eca446b..13cd3318c5 100644 --- a/cg/cli/transfer.py +++ b/cg/cli/transfer.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.apps.lims import LimsAPI from cg.cli.utils import CLICK_CONTEXT_SETTINGS diff --git a/cg/cli/upload/base.py b/cg/cli/upload/base.py index 4840e1af7d..4bf46d6303 100644 --- a/cg/cli/upload/base.py +++ b/cg/cli/upload/base.py @@ -4,7 +4,7 @@ import sys import traceback -import click +import rich_click as click from cg.cli.upload.coverage import upload_coverage from cg.cli.upload.delivery_report import upload_delivery_report_to_scout diff --git a/cg/cli/upload/coverage.py b/cg/cli/upload/coverage.py index f37ed7b028..7e53e1581b 100644 --- a/cg/cli/upload/coverage.py +++ b/cg/cli/upload/coverage.py @@ -1,6 +1,6 @@ """Code for uploading coverage reports via CLI""" -import click +import rich_click as click from cg.meta.upload.coverage import UploadCoverageApi from cg.models.cg_config import CGConfig diff --git a/cg/cli/upload/delivery_report.py b/cg/cli/upload/delivery_report.py index 25e8d15c5a..03cf990558 100644 --- a/cg/cli/upload/delivery_report.py +++ b/cg/cli/upload/delivery_report.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from housekeeper.store.models import Version from cg.cli.generate.delivery_report.options import ARGUMENT_CASE_ID diff --git a/cg/cli/upload/fohm.py b/cg/cli/upload/fohm.py index 194b610cdb..6571e0be8c 100644 --- a/cg/cli/upload/fohm.py +++ b/cg/cli/upload/fohm.py @@ -1,6 +1,6 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.constants.cli_options import DRY_RUN diff --git a/cg/cli/upload/genotype.py b/cg/cli/upload/genotype.py index ee79a25641..9371c51a6c 100644 --- a/cg/cli/upload/genotype.py +++ b/cg/cli/upload/genotype.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.apps.gt import GenotypeAPI from cg.apps.housekeeper.hk import HousekeeperAPI diff --git a/cg/cli/upload/gens.py b/cg/cli/upload/gens.py index a2ae7f55c0..ad317c4bae 100644 --- a/cg/cli/upload/gens.py +++ b/cg/cli/upload/gens.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from housekeeper.store.models import File from cg.apps.gens import GensAPI diff --git a/cg/cli/upload/gisaid.py b/cg/cli/upload/gisaid.py index 96b4e24d63..0a4461023d 100644 --- a/cg/cli/upload/gisaid.py +++ b/cg/cli/upload/gisaid.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.meta.upload.gisaid import GisaidAPI from cg.models.cg_config import CGConfig diff --git a/cg/cli/upload/mutacc.py b/cg/cli/upload/mutacc.py index c8f315fd52..a57aee91a6 100644 --- a/cg/cli/upload/mutacc.py +++ b/cg/cli/upload/mutacc.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.apps.mutacc_auto import MutaccAutoAPI from cg.apps.scout.scout_export import ScoutExportCase diff --git a/cg/cli/upload/nipt/base.py b/cg/cli/upload/nipt/base.py index 7c76e97158..72fd2ad482 100644 --- a/cg/cli/upload/nipt/base.py +++ b/cg/cli/upload/nipt/base.py @@ -3,7 +3,7 @@ import logging import traceback -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.constants.cli_options import DRY_RUN, FORCE diff --git a/cg/cli/upload/nipt/ftp.py b/cg/cli/upload/nipt/ftp.py index bfd69c2f47..12c07ac7de 100644 --- a/cg/cli/upload/nipt/ftp.py +++ b/cg/cli/upload/nipt/ftp.py @@ -1,7 +1,7 @@ import logging from pathlib import Path -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.constants.cli_options import DRY_RUN, FORCE diff --git a/cg/cli/upload/nipt/statina.py b/cg/cli/upload/nipt/statina.py index febe3f15c3..56f52be463 100644 --- a/cg/cli/upload/nipt/statina.py +++ b/cg/cli/upload/nipt/statina.py @@ -1,6 +1,6 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.constants.cli_options import DRY_RUN, FORCE diff --git a/cg/cli/upload/observations/observations.py b/cg/cli/upload/observations/observations.py index 1f51e40dda..abddad3c38 100644 --- a/cg/cli/upload/observations/observations.py +++ b/cg/cli/upload/observations/observations.py @@ -3,7 +3,7 @@ import logging from datetime import datetime -import click +import rich_click as click from sqlalchemy.orm import Query from cg.cli.upload.observations.utils import get_observations_api diff --git a/cg/cli/upload/scout.py b/cg/cli/upload/scout.py index a8a4064609..88812528ca 100644 --- a/cg/cli/upload/scout.py +++ b/cg/cli/upload/scout.py @@ -3,7 +3,7 @@ import logging from pathlib import Path -import click +import rich_click as click from housekeeper.store.models import File, Version from cg.apps.housekeeper.hk import HousekeeperAPI diff --git a/cg/cli/upload/utils.py b/cg/cli/upload/utils.py index 5276b47475..8ca6db9d28 100644 --- a/cg/cli/upload/utils.py +++ b/cg/cli/upload/utils.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.constants import Workflow from cg.constants.constants import MAX_ITEMS_TO_RETRIEVE diff --git a/cg/cli/upload/validate.py b/cg/cli/upload/validate.py index 220934153e..bd2bd09055 100644 --- a/cg/cli/upload/validate.py +++ b/cg/cli/upload/validate.py @@ -1,6 +1,6 @@ """Code for validating an upload via CLI""" -import click +import rich_click as click from cg.apps.coverage import ChanjoAPI from cg.models.cg_config import CGConfig diff --git a/cg/cli/utils.py b/cg/cli/utils.py index 6e55703502..be13b5c52b 100644 --- a/cg/cli/utils.py +++ b/cg/cli/utils.py @@ -1,7 +1,7 @@ import re import shutil -import click +import rich_click as click from cg.constants import Workflow from cg.meta.workflow.raredisease import RarediseaseAnalysisAPI diff --git a/cg/cli/workflow/balsamic/base.py b/cg/cli/workflow/balsamic/base.py index b3028c1c30..afb18bc185 100644 --- a/cg/cli/workflow/balsamic/base.py +++ b/cg/cli/workflow/balsamic/base.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from pydantic.v1 import ValidationError from cg.apps.housekeeper.hk import HousekeeperAPI diff --git a/cg/cli/workflow/balsamic/options.py b/cg/cli/workflow/balsamic/options.py index 744393ee38..374640ecfe 100644 --- a/cg/cli/workflow/balsamic/options.py +++ b/cg/cli/workflow/balsamic/options.py @@ -1,4 +1,4 @@ -import click +import rich_click as click from cg.constants.constants import GenomeVersion from cg.constants.priority import SlurmQos diff --git a/cg/cli/workflow/balsamic/pon.py b/cg/cli/workflow/balsamic/pon.py index 6c07b35925..186a3c038b 100644 --- a/cg/cli/workflow/balsamic/pon.py +++ b/cg/cli/workflow/balsamic/pon.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.workflow.balsamic.base import config_case, run, start diff --git a/cg/cli/workflow/balsamic/qc.py b/cg/cli/workflow/balsamic/qc.py index 2ea997e454..e2690ce334 100644 --- a/cg/cli/workflow/balsamic/qc.py +++ b/cg/cli/workflow/balsamic/qc.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.workflow.balsamic.base import ( diff --git a/cg/cli/workflow/balsamic/umi.py b/cg/cli/workflow/balsamic/umi.py index b5b4914fa2..34df4328b1 100644 --- a/cg/cli/workflow/balsamic/umi.py +++ b/cg/cli/workflow/balsamic/umi.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.workflow.balsamic.base import ( diff --git a/cg/cli/workflow/base.py b/cg/cli/workflow/base.py index 98b2451e8b..e159534808 100644 --- a/cg/cli/workflow/base.py +++ b/cg/cli/workflow/base.py @@ -1,6 +1,6 @@ """Common CLI workflow functions""" -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.workflow.balsamic.base import balsamic diff --git a/cg/cli/workflow/commands.py b/cg/cli/workflow/commands.py index 8172181585..3d30b7e221 100644 --- a/cg/cli/workflow/commands.py +++ b/cg/cli/workflow/commands.py @@ -3,7 +3,7 @@ import shutil from pathlib import Path -import click +import rich_click as click from dateutil.parser import parse as parse_date from cg.apps.housekeeper.hk import HousekeeperAPI diff --git a/cg/cli/workflow/fluffy/base.py b/cg/cli/workflow/fluffy/base.py index 1302f711d2..6da80b5639 100644 --- a/cg/cli/workflow/fluffy/base.py +++ b/cg/cli/workflow/fluffy/base.py @@ -1,6 +1,6 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.workflow.commands import link, resolve_compression, store, store_available diff --git a/cg/cli/workflow/jasen/base.py b/cg/cli/workflow/jasen/base.py index c4aafee671..c8a9fb747c 100644 --- a/cg/cli/workflow/jasen/base.py +++ b/cg/cli/workflow/jasen/base.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.constants.constants import MetaApis diff --git a/cg/cli/workflow/microsalt/base.py b/cg/cli/workflow/microsalt/base.py index a34e76fa8d..474feb8e57 100644 --- a/cg/cli/workflow/microsalt/base.py +++ b/cg/cli/workflow/microsalt/base.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Any -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.workflow.commands import resolve_compression, store, store_available diff --git a/cg/cli/workflow/mip/base.py b/cg/cli/workflow/mip/base.py index 924310283d..ac6ac3e2c5 100644 --- a/cg/cli/workflow/mip/base.py +++ b/cg/cli/workflow/mip/base.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.apps.environ import environ_email from cg.cli.utils import echo_lines diff --git a/cg/cli/workflow/mip/options.py b/cg/cli/workflow/mip/options.py index 5a99124c28..7ad022fd81 100644 --- a/cg/cli/workflow/mip/options.py +++ b/cg/cli/workflow/mip/options.py @@ -1,4 +1,4 @@ -import click +import rich_click as click from cg.constants.priority import SlurmQos diff --git a/cg/cli/workflow/mip_dna/base.py b/cg/cli/workflow/mip_dna/base.py index de629558e6..eb503cc77e 100644 --- a/cg/cli/workflow/mip_dna/base.py +++ b/cg/cli/workflow/mip_dna/base.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.workflow.commands import ( diff --git a/cg/cli/workflow/mip_rna/base.py b/cg/cli/workflow/mip_rna/base.py index 62c1d9d97e..a345c83e7e 100644 --- a/cg/cli/workflow/mip_rna/base.py +++ b/cg/cli/workflow/mip_rna/base.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.workflow.commands import link, resolve_compression, store, store_available diff --git a/cg/cli/workflow/mutant/base.py b/cg/cli/workflow/mutant/base.py index 740eb16e2e..e2f7f9310f 100644 --- a/cg/cli/workflow/mutant/base.py +++ b/cg/cli/workflow/mutant/base.py @@ -1,6 +1,6 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.workflow.commands import ( diff --git a/cg/cli/workflow/nf_analysis.py b/cg/cli/workflow/nf_analysis.py index 7011df85b2..f645975923 100644 --- a/cg/cli/workflow/nf_analysis.py +++ b/cg/cli/workflow/nf_analysis.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from pydantic import ValidationError from cg.cli.workflow.commands import ARGUMENT_CASE_ID diff --git a/cg/cli/workflow/raredisease/base.py b/cg/cli/workflow/raredisease/base.py index 7910a8a3e0..172f225557 100644 --- a/cg/cli/workflow/raredisease/base.py +++ b/cg/cli/workflow/raredisease/base.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS, echo_lines from cg.cli.workflow.commands import ARGUMENT_CASE_ID, resolve_compression diff --git a/cg/cli/workflow/raw_data/base.py b/cg/cli/workflow/raw_data/base.py index a771b09014..490db36db7 100644 --- a/cg/cli/workflow/raw_data/base.py +++ b/cg/cli/workflow/raw_data/base.py @@ -1,6 +1,6 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.workflow.commands import ARGUMENT_CASE_ID diff --git a/cg/cli/workflow/rnafusion/base.py b/cg/cli/workflow/rnafusion/base.py index 27901d336e..18f9977feb 100644 --- a/cg/cli/workflow/rnafusion/base.py +++ b/cg/cli/workflow/rnafusion/base.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.workflow.commands import resolve_compression diff --git a/cg/cli/workflow/taxprofiler/base.py b/cg/cli/workflow/taxprofiler/base.py index 14a6782d67..6c22a8b8bb 100644 --- a/cg/cli/workflow/taxprofiler/base.py +++ b/cg/cli/workflow/taxprofiler/base.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.workflow.commands import resolve_compression diff --git a/cg/cli/workflow/tomte/base.py b/cg/cli/workflow/tomte/base.py index 4fc3a0c85c..0a0ab8a4e2 100644 --- a/cg/cli/workflow/tomte/base.py +++ b/cg/cli/workflow/tomte/base.py @@ -2,7 +2,7 @@ import logging -import click +import rich_click as click from cg.cli.utils import CLICK_CONTEXT_SETTINGS from cg.cli.workflow.commands import resolve_compression diff --git a/cg/cli/workflow/utils.py b/cg/cli/workflow/utils.py index 832266712e..bbcc0f6298 100644 --- a/cg/cli/workflow/utils.py +++ b/cg/cli/workflow/utils.py @@ -1,6 +1,6 @@ """CLI utility methods.""" -import click +import rich_click as click def validate_force_store_option(force: bool, comment: str | None): diff --git a/cg/constants/cli_options.py b/cg/constants/cli_options.py index d6976797fe..d03116a35a 100644 --- a/cg/constants/cli_options.py +++ b/cg/constants/cli_options.py @@ -1,4 +1,4 @@ -import click +import rich_click as click DRY_RUN = click.option( "--dry-run", diff --git a/cg/meta/archive/archive.py b/cg/meta/archive/archive.py index b744d5e0d6..553ab98bda 100644 --- a/cg/meta/archive/archive.py +++ b/cg/meta/archive/archive.py @@ -1,7 +1,7 @@ import logging from typing import Callable, Type -import click +import rich_click as click from housekeeper.store.models import Archive, File from pydantic import BaseModel, ConfigDict diff --git a/cg/meta/upload/balsamic/balsamic.py b/cg/meta/upload/balsamic/balsamic.py index c9ce637fba..f297a367bc 100644 --- a/cg/meta/upload/balsamic/balsamic.py +++ b/cg/meta/upload/balsamic/balsamic.py @@ -3,7 +3,7 @@ import datetime as dt import logging -import click +import rich_click as click from cg.apps.gens import GensAPI from cg.cli.generate.delivery_report.base import generate_delivery_report diff --git a/cg/meta/upload/microsalt/microsalt_upload_api.py b/cg/meta/upload/microsalt/microsalt_upload_api.py index f2d35a1f24..9bc966d9ad 100644 --- a/cg/meta/upload/microsalt/microsalt_upload_api.py +++ b/cg/meta/upload/microsalt/microsalt_upload_api.py @@ -1,6 +1,6 @@ import logging -import click +import rich_click as click from cg.meta.upload.upload_api import UploadAPI diff --git a/cg/meta/upload/mip/mip_dna.py b/cg/meta/upload/mip/mip_dna.py index 9b045b6448..423d9cf30f 100644 --- a/cg/meta/upload/mip/mip_dna.py +++ b/cg/meta/upload/mip/mip_dna.py @@ -3,7 +3,7 @@ import datetime as dt import logging -import click +import rich_click as click from cg.cli.generate.delivery_report.base import generate_delivery_report from cg.cli.upload.coverage import upload_coverage diff --git a/cg/meta/upload/mip/mip_rna.py b/cg/meta/upload/mip/mip_rna.py index 8223242a5f..ddfacff544 100644 --- a/cg/meta/upload/mip/mip_rna.py +++ b/cg/meta/upload/mip/mip_rna.py @@ -3,7 +3,7 @@ import logging from subprocess import CalledProcessError -import click +import rich_click as click from cg.cli.upload.scout import upload_rna_to_scout diff --git a/cg/meta/upload/nf_analysis.py b/cg/meta/upload/nf_analysis.py index 11da3a7ee8..a9e90fef56 100644 --- a/cg/meta/upload/nf_analysis.py +++ b/cg/meta/upload/nf_analysis.py @@ -3,7 +3,7 @@ import datetime as dt import logging -import click +import rich_click as click from cg.cli.generate.delivery_report.base import generate_delivery_report from cg.cli.upload.scout import upload_to_scout diff --git a/cg/meta/upload/raredisease/raredisease.py b/cg/meta/upload/raredisease/raredisease.py index 3646daed11..17bea8738b 100644 --- a/cg/meta/upload/raredisease/raredisease.py +++ b/cg/meta/upload/raredisease/raredisease.py @@ -3,7 +3,7 @@ import datetime as dt import logging -import click +import rich_click as click from cg.cli.generate.delivery_report.base import generate_delivery_report from cg.cli.upload.genotype import upload_genotypes diff --git a/cg/meta/upload/tomte/tomte.py b/cg/meta/upload/tomte/tomte.py index 57cad6fbc4..f676c93dc5 100644 --- a/cg/meta/upload/tomte/tomte.py +++ b/cg/meta/upload/tomte/tomte.py @@ -3,7 +3,7 @@ import logging from subprocess import CalledProcessError -import click +import rich_click as click from cg.cli.generate.delivery_report.base import generate_delivery_report from cg.cli.upload.scout import upload_tomte_to_scout diff --git a/cg/meta/upload/upload_api.py b/cg/meta/upload/upload_api.py index 4593395ad1..d455079f92 100644 --- a/cg/meta/upload/upload_api.py +++ b/cg/meta/upload/upload_api.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta from pathlib import Path -import click +import rich_click as click from cg.exc import AnalysisAlreadyUploadedError, AnalysisUploadError from cg.meta.meta import MetaAPI diff --git a/cg/meta/workflow/analysis.py b/cg/meta/workflow/analysis.py index e64d68db0d..b650383b7e 100644 --- a/cg/meta/workflow/analysis.py +++ b/cg/meta/workflow/analysis.py @@ -6,7 +6,7 @@ from subprocess import CalledProcessError from typing import Iterator -import click +import rich_click as click from housekeeper.store.models import Bundle, Version from cg.apps.environ import environ_email diff --git a/cg/meta/workflow/microsalt/microsalt.py b/cg/meta/workflow/microsalt/microsalt.py index 59f191d485..c3cd228bed 100644 --- a/cg/meta/workflow/microsalt/microsalt.py +++ b/cg/meta/workflow/microsalt/microsalt.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Any -import click +import rich_click as click from cg.constants import EXIT_FAIL, EXIT_SUCCESS, Priority, Workflow from cg.constants.constants import FileExtensions diff --git a/cg/utils/click/EnumChoice.py b/cg/utils/click/EnumChoice.py index 0f77dd639f..5a3a64dd0c 100644 --- a/cg/utils/click/EnumChoice.py +++ b/cg/utils/click/EnumChoice.py @@ -4,7 +4,7 @@ from enum import EnumMeta -import click +import rich_click as click class EnumChoice(click.Choice): diff --git a/poetry.lock b/poetry.lock index 7903d670bf..7df6adc182 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "alembic" @@ -1990,6 +1990,26 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "rich-click" +version = "1.8.4" +description = "Format click help output nicely with rich" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rich_click-1.8.4-py3-none-any.whl", hash = "sha256:2d2841b3cebe610d5682baa1194beaf78ab00c4fa31931533261b5eba2ee80b7"}, + {file = "rich_click-1.8.4.tar.gz", hash = "sha256:0f49471f04439269d0e66a6f43120f52d11d594869a2a0be600cfb12eb0616b9"}, +] + +[package.dependencies] +click = ">=7" +rich = ">=10.7" +typing-extensions = ">=4" + +[package.extras] +dev = ["mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "rich-codex", "ruff", "types-setuptools"] +docs = ["markdown-include", "mkdocs", "mkdocs-glightbox", "mkdocs-material-extensions", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-rss-plugin", "mkdocstrings[python]", "rich-codex"] + [[package]] name = "rsa" version = "4.9" @@ -2288,4 +2308,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "7849ceb358ed7655482144407fa2b0fc2a11091906b696c957b283480415e9a1" +content-hash = "3b7bcec9c2a1a49b6a5a0f7cc3018d1f63bccff0d8fdf6cac6f139876933dc1b" diff --git a/pyproject.toml b/pyproject.toml index b35e610173..a6e4424240 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,7 @@ genologics = "*" housekeeper = ">=4.11.3" pydantic-settings = "^2.3.3" email-validator = "^2.2.0" +rich-click = "^1.8.4" [tool.poetry.dev-dependencies] diff --git a/tests/cli/demultiplex/test_validate_sample_sheet.py b/tests/cli/demultiplex/test_validate_sample_sheet.py index 7ccb32daa2..c2874d965e 100644 --- a/tests/cli/demultiplex/test_validate_sample_sheet.py +++ b/tests/cli/demultiplex/test_validate_sample_sheet.py @@ -19,7 +19,7 @@ def test_validate_non_existing_sample_sheet( sample_sheet: Path = Path("a_sample_sheet_that_does_not_exist.csv") assert sample_sheet.exists() is False - # WHEN validating the sample sheet + # WHEN validating the sample sheetresult = {Result} result = cli_runner.invoke( validate_sample_sheet, [str(sample_sheet)], @@ -28,8 +28,6 @@ def test_validate_non_existing_sample_sheet( # THEN assert that it exits with a non-zero exit code assert result.exit_code != EXIT_SUCCESS - # THEN assert the correct information was communicated - assert f"File '{sample_sheet.name}' does not exist" in result.output def test_validate_sample_sheet_wrong_file_type( diff --git a/tests/fixture_plugins/delivery_report_fixtures/context_fixtures.py b/tests/fixture_plugins/delivery_report_fixtures/context_fixtures.py index 2831f3fddf..dd6d5b06bd 100644 --- a/tests/fixture_plugins/delivery_report_fixtures/context_fixtures.py +++ b/tests/fixture_plugins/delivery_report_fixtures/context_fixtures.py @@ -3,7 +3,7 @@ from datetime import datetime from pathlib import Path -import click +import rich_click as click import pytest from pytest_mock import MockFixture From 0fadd5852bf43e98a5c70f87c938439fdf40d63b Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Thu, 28 Nov 2024 14:32:50 +0000 Subject: [PATCH 11/25] =?UTF-8?q?Bump=20version:=2064.5.16=20=E2=86=92=206?= =?UTF-8?q?4.5.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 c9a5308898..c3cd96cb33 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 64.5.16 +current_version = 64.5.17 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index 38fb70b985..427a1019d5 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "64.5.16" +__version__ = "64.5.17" diff --git a/pyproject.toml b/pyproject.toml index a6e4424240..5efde72096 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "cg" -version = "64.5.16" +version = "64.5.17" description = "Clinical Genomics command center" authors = ["Clinical Genomics "] readme = "README.md" From 5aba7cb3271a95f53b316203634c8021a489f40a Mon Sep 17 00:00:00 2001 From: Beatriz Vinhas Date: Thu, 28 Nov 2024 15:58:27 +0100 Subject: [PATCH 12/25] Add mip rna messages (#3902) (patch) ### Added - crud functions to retrieve dna cases related to an case. - Logic for generation of delivery messages for mip-rna. --- cg/constants/__init__.py | 1 + cg/constants/constants.py | 7 ++ cg/constants/sequencing.py | 7 ++ cg/server/endpoints/cases.py | 8 +- .../delivery_message_service.py | 6 +- .../delivery_message/messages/__init__.py | 24 ++++-- .../messages/rna_delivery_message.py | 81 +++++++++++++++++++ .../delivery_message/messages/utils.py | 5 ++ cg/services/delivery_message/utils.py | 80 +++++++++++------- cg/store/crud/read.py | 79 +++++++++++------- .../filters/status_case_sample_filters.py | 2 + cg/store/filters/status_sample_filters.py | 6 ++ tests/store/crud/conftest.py | 47 ++++++++--- tests/store/crud/read/test_read_case.py | 33 +++----- tests/store/crud/read/test_read_sample.py | 17 ++-- 15 files changed, 294 insertions(+), 109 deletions(-) create mode 100644 cg/services/delivery_message/messages/rna_delivery_message.py diff --git a/cg/constants/__init__.py b/cg/constants/__init__.py index 8901dbc8e6..4c13d30875 100644 --- a/cg/constants/__init__.py +++ b/cg/constants/__init__.py @@ -6,6 +6,7 @@ CAPTUREKIT_OPTIONS, CONTAINER_OPTIONS, DEFAULT_CAPTURE_KIT, + DNA_WORKFLOWS_WITH_SCOUT_UPLOAD, STATUS_OPTIONS, DataDelivery, FileExtensions, diff --git a/cg/constants/constants.py b/cg/constants/constants.py index 210449a2c7..8a8e573b6d 100644 --- a/cg/constants/constants.py +++ b/cg/constants/constants.py @@ -140,6 +140,13 @@ class Workflow(StrEnum): TOMTE: str = "tomte" +DNA_WORKFLOWS_WITH_SCOUT_UPLOAD: list[Workflow] = [ + Workflow.MIP_DNA, + Workflow.BALSAMIC, + Workflow.BALSAMIC_UMI, +] + + class FileFormat(StrEnum): CSV: str = "csv" FASTQ: str = "fastq" diff --git a/cg/constants/sequencing.py b/cg/constants/sequencing.py index e22b9652ab..e3361f9821 100644 --- a/cg/constants/sequencing.py +++ b/cg/constants/sequencing.py @@ -72,3 +72,10 @@ class SeqLibraryPrepCategory(StrEnum): WHOLE_EXOME_SEQUENCING: str = "wes" WHOLE_GENOME_SEQUENCING: str = "wgs" WHOLE_TRANSCRIPTOME_SEQUENCING: str = "wts" + + +DNA_PREP_CATEGORIES: list[SeqLibraryPrepCategory] = [ + SeqLibraryPrepCategory.WHOLE_GENOME_SEQUENCING, + SeqLibraryPrepCategory.TARGETED_GENOME_SEQUENCING, + SeqLibraryPrepCategory.WHOLE_EXOME_SEQUENCING, +] diff --git a/cg/server/endpoints/cases.py b/cg/server/endpoints/cases.py index 4d03f47c43..c81dda2e1d 100644 --- a/cg/server/endpoints/cases.py +++ b/cg/server/endpoints/cases.py @@ -1,7 +1,9 @@ import logging from http import HTTPStatus + from flask import Blueprint, abort, g, jsonify, request -from cg.exc import CaseNotFoundError, OrderMismatchError + +from cg.exc import CaseNotFoundError, CgDataError, OrderMismatchError from cg.server.dto.delivery_message.delivery_message_request import DeliveryMessageRequest from cg.server.dto.delivery_message.delivery_message_response import DeliveryMessageResponse from cg.server.endpoints.utils import before_request @@ -62,7 +64,7 @@ def get_cases_delivery_message(): delivery_message_request ) return jsonify(response.model_dump()), HTTPStatus.OK - except (CaseNotFoundError, OrderMismatchError) as error: + except (CaseNotFoundError, OrderMismatchError, CgDataError) as error: return jsonify({"error": str(error)}), HTTPStatus.BAD_REQUEST @@ -74,7 +76,7 @@ def get_case_delivery_message(case_id: str): delivery_message_request ) return jsonify(response.model_dump()), HTTPStatus.OK - except CaseNotFoundError as error: + except (CaseNotFoundError, CgDataError) as error: return jsonify({"error": str(error)}), HTTPStatus.BAD_REQUEST diff --git a/cg/services/delivery_message/delivery_message_service.py b/cg/services/delivery_message/delivery_message_service.py index 3d00b22f39..e0682162ea 100644 --- a/cg/services/delivery_message/delivery_message_service.py +++ b/cg/services/delivery_message/delivery_message_service.py @@ -1,9 +1,7 @@ from cg.apps.tb import TrailblazerAPI from cg.apps.tb.models import TrailblazerAnalysis from cg.exc import OrderNotDeliverableError -from cg.server.dto.delivery_message.delivery_message_request import ( - DeliveryMessageRequest, -) +from cg.server.dto.delivery_message.delivery_message_request import DeliveryMessageRequest from cg.server.dto.delivery_message.delivery_message_response import ( DeliveryMessageOrderResponse, DeliveryMessageResponse, @@ -54,5 +52,5 @@ def _get_validated_analyses( def _get_delivery_message(self, case_ids: set[str]) -> str: cases: list[Case] = self.store.get_cases_by_internal_ids(case_ids) validate_cases(cases=cases, case_ids=case_ids) - message: str = get_message(cases) + message: str = get_message(cases=cases, store=self.store) return message diff --git a/cg/services/delivery_message/messages/__init__.py b/cg/services/delivery_message/messages/__init__.py index 1c6794b367..88520919fe 100644 --- a/cg/services/delivery_message/messages/__init__.py +++ b/cg/services/delivery_message/messages/__init__.py @@ -1,11 +1,25 @@ -from cg.services.delivery_message.messages.analysis_scout_message import AnalysisScoutMessage +from cg.services.delivery_message.messages.analysis_scout_message import ( + AnalysisScoutMessage, +) from cg.services.delivery_message.messages.covid_message import CovidMessage -from cg.services.delivery_message.messages.fastq_message import FastqMessage -from cg.services.delivery_message.messages.fastq_scout_message import FastqScoutMessage from cg.services.delivery_message.messages.fastq_analysis_scout_message import ( FastqAnalysisScoutMessage, ) -from cg.services.delivery_message.messages.microsalt_mwr_message import MicrosaltMwrMessage -from cg.services.delivery_message.messages.microsalt_mwx_message import MicrosaltMwxMessage +from cg.services.delivery_message.messages.fastq_message import FastqMessage +from cg.services.delivery_message.messages.fastq_scout_message import FastqScoutMessage +from cg.services.delivery_message.messages.microsalt_mwr_message import ( + MicrosaltMwrMessage, +) +from cg.services.delivery_message.messages.microsalt_mwx_message import ( + MicrosaltMwxMessage, +) from cg.services.delivery_message.messages.scout_message import ScoutMessage from cg.services.delivery_message.messages.statina_message import StatinaMessage +from cg.services.delivery_message.messages.rna_delivery_message import ( + RNAScoutStrategy, + RNAFastqStrategy, + RNAAnalysisStrategy, + RNAFastqAnalysisStrategy, + RNAUploadMessageStrategy, + RNADeliveryMessage, +) diff --git a/cg/services/delivery_message/messages/rna_delivery_message.py b/cg/services/delivery_message/messages/rna_delivery_message.py new file mode 100644 index 0000000000..04461d0391 --- /dev/null +++ b/cg/services/delivery_message/messages/rna_delivery_message.py @@ -0,0 +1,81 @@ +from abc import abstractmethod, ABC + +from cg.services.delivery_message.messages.utils import ( + get_scout_links_row_separated, + get_caesar_delivery_path, +) +from cg.store.models import Case + + +class RNAUploadMessageStrategy(ABC): + """Abstract base class for delivery message strategies.""" + + @abstractmethod + def get_file_upload_message(self, delivery_path: str) -> str: + """Generate the file upload message part.""" + pass + + +class RNAAnalysisStrategy(RNAUploadMessageStrategy): + def get_file_upload_message(self, delivery_path: str) -> str: + return ( + f"The analysis files are currently being uploaded to your inbox on Caesar:\n\n" + f"{delivery_path}" + ) + + +class RNAFastqAnalysisStrategy(RNAUploadMessageStrategy): + def get_file_upload_message(self, delivery_path: str) -> str: + return ( + f"The fastq and analysis files are currently being uploaded to your inbox on Caesar:\n\n" + f"{delivery_path}" + ) + + +class RNAFastqStrategy(RNAUploadMessageStrategy): + def get_file_upload_message(self, delivery_path: str) -> str: + return ( + f"The fastq files are currently being uploaded to your inbox on Caesar:\n\n" + f"{delivery_path}" + ) + + +class RNAScoutStrategy(RNAUploadMessageStrategy): + def get_file_upload_message(self, delivery_path: str) -> str: + return "" # No file upload message needed for this case. + + +class RNADeliveryMessage: + def __init__(self, store, strategy: RNAUploadMessageStrategy): + self.store = store + self.strategy = strategy + + def create_message(self, cases: list[Case]) -> str: + if len(cases) == 1: + return self._get_case_message(cases[0]) + return self._get_cases_message(cases) + + def _get_case_message(self, case: Case) -> str: + related_uploaded_dna_cases = self.store.get_uploaded_related_dna_cases(rna_case=case) + scout_links = get_scout_links_row_separated(cases=related_uploaded_dna_cases) + delivery_path = get_caesar_delivery_path(case) + file_upload_message = self.strategy.get_file_upload_message(delivery_path) + return ( + f"Hello,\n\n" + f"The analysis for case {case.name} has been uploaded to the corresponding DNA case(s) on Scout at:\n\n" + f"{scout_links}\n\n" + f"{file_upload_message}" + ) + + def _get_cases_message(self, cases: list[Case]) -> str: + message = "Hello,\n\n" + for case in cases: + related_uploaded_dna_cases = self.store.get_uploaded_related_dna_cases(rna_case=case) + scout_links = get_scout_links_row_separated(cases=related_uploaded_dna_cases) + message += ( + f"The analysis for case {case.name} has been uploaded to the corresponding DNA case(s) on Scout at:\n\n" + f"{scout_links}\n\n" + ) + delivery_path = get_caesar_delivery_path(cases[0]) + file_upload_message = self.strategy.get_file_upload_message(delivery_path) + return message + file_upload_message diff --git a/cg/services/delivery_message/messages/utils.py b/cg/services/delivery_message/messages/utils.py index 09b47aed98..20ac60035d 100644 --- a/cg/services/delivery_message/messages/utils.py +++ b/cg/services/delivery_message/messages/utils.py @@ -13,6 +13,11 @@ def get_scout_link(case: Case) -> str: return f"https://scout.scilifelab.se/{customer_id}/{case_name}" +def get_scout_links_row_separated(cases: list[Case]) -> str: + scout_links: list[str] = [get_scout_link(case) for case in cases] + return "\n".join(scout_links) + + def get_pangolin_delivery_path(case: Case) -> str: customer_id: str = case.customer.internal_id return f"/home/{customer_id}/inbox/wwLab_automatisk_hamtning" diff --git a/cg/services/delivery_message/utils.py b/cg/services/delivery_message/utils.py index 04ad891379..f3cfa87c41 100644 --- a/cg/services/delivery_message/utils.py +++ b/cg/services/delivery_message/utils.py @@ -1,9 +1,5 @@ from cg.constants.constants import DataDelivery, MicrosaltAppTags, Workflow -from cg.exc import ( - CaseNotFoundError, - DeliveryMessageNotSupportedError, - OrderMismatchError, -) +from cg.exc import CaseNotFoundError, OrderMismatchError from cg.services.delivery_message.messages import ( AnalysisScoutMessage, CovidMessage, @@ -17,47 +13,76 @@ from cg.services.delivery_message.messages.analysis_message import AnalysisMessage from cg.services.delivery_message.messages.bam_message import BamMessage from cg.services.delivery_message.messages.delivery_message import DeliveryMessage -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.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, + RNAFastqAnalysisStrategy, + RNAUploadMessageStrategy, + RNADeliveryMessage, ) from cg.store.models import Case, Sample +from cg.store.store import Store + +MESSAGE_MAP = { + DataDelivery.ANALYSIS_FILES: AnalysisMessage, + DataDelivery.FASTQ: FastqMessage, + DataDelivery.SCOUT: ScoutMessage, + DataDelivery.FASTQ_SCOUT: FastqScoutMessage, + DataDelivery.FASTQ_ANALYSIS: FastqAnalysisMessage, + DataDelivery.ANALYSIS_SCOUT: AnalysisScoutMessage, + DataDelivery.FASTQ_ANALYSIS_SCOUT: FastqAnalysisScoutMessage, + DataDelivery.STATINA: StatinaMessage, + DataDelivery.BAM: BamMessage, +} -def get_message(cases: list[Case]) -> str: - message_strategy: DeliveryMessage = get_message_strategy(cases[0]) +RNA_STRATEGY_MAP: dict[DataDelivery, type[RNAUploadMessageStrategy]] = { + # Only returns a message strategy if there is a scout delivery for the case. + DataDelivery.SCOUT: RNAScoutStrategy, + DataDelivery.FASTQ_SCOUT: RNAFastqStrategy, + DataDelivery.ANALYSIS_SCOUT: RNAAnalysisStrategy, + DataDelivery.FASTQ_ANALYSIS_SCOUT: RNAFastqAnalysisStrategy, +} + + +def get_message(cases: list[Case], store: Store) -> str: + message_strategy: DeliveryMessage = get_message_strategy(cases[0], store) return message_strategy.create_message(cases) -def get_message_strategy(case: Case) -> DeliveryMessage: +def get_message_strategy(case: Case, store: Store) -> DeliveryMessage: if case.data_analysis == Workflow.MICROSALT: return get_microsalt_message_strategy(case) if case.data_analysis == Workflow.MUTANT: return CovidMessage() + if case.data_analysis == Workflow.MIP_RNA: + return get_rna_message_strategy_from_data_delivery(case=case, store=store) + message_strategy: DeliveryMessage = get_message_strategy_from_data_delivery(case) return message_strategy def get_message_strategy_from_data_delivery(case: Case) -> DeliveryMessage: - message_strategy: DeliveryMessage = message_map[case.data_delivery]() + message_strategy: DeliveryMessage = MESSAGE_MAP[case.data_delivery]() return message_strategy -message_map = { - DataDelivery.ANALYSIS_FILES: AnalysisMessage, - DataDelivery.FASTQ: FastqMessage, - DataDelivery.SCOUT: ScoutMessage, - DataDelivery.FASTQ_SCOUT: FastqScoutMessage, - DataDelivery.FASTQ_ANALYSIS: FastqAnalysisMessage, - DataDelivery.ANALYSIS_SCOUT: AnalysisScoutMessage, - DataDelivery.FASTQ_ANALYSIS_SCOUT: FastqAnalysisScoutMessage, - DataDelivery.STATINA: StatinaMessage, - DataDelivery.BAM: BamMessage, -} +def get_rna_message_strategy_from_data_delivery( + case: Case, store: Store +) -> DeliveryMessage | RNADeliveryMessage: + """Get the RNA delivery message strategy based on the data delivery type. + 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: + return RNADeliveryMessage(store=store, strategy=message_strategy()) + return MESSAGE_MAP[case.data_delivery]() def get_microsalt_message_strategy(case: Case) -> DeliveryMessage: @@ -100,11 +125,6 @@ def validate_cases(cases: list[Case], case_ids: list[str]) -> None: raise CaseNotFoundError("Internal id not found in the database") if not is_matching_order(cases): raise OrderMismatchError("Cases do not belong to the same order") - cases_with_mip_rna: list[Case] = [ - case for case in cases if case.data_analysis == Workflow.MIP_RNA - ] - if cases_with_mip_rna: - raise DeliveryMessageNotSupportedError("Workflow is not supported.") def is_matching_order(cases: list[Case]) -> bool: diff --git a/cg/store/crud/read.py b/cg/store/crud/read.py index e567019259..741ff7bcc6 100644 --- a/cg/store/crud/read.py +++ b/cg/store/crud/read.py @@ -8,8 +8,13 @@ from sqlalchemy.orm import Query, Session from cg.constants import SequencingRunDataAvailability, Workflow -from cg.constants.constants import CaseActions, CustomerId, SampleType -from cg.constants.sequencing import SeqLibraryPrepCategory +from cg.constants.constants import ( + DNA_WORKFLOWS_WITH_SCOUT_UPLOAD, + CaseActions, + CustomerId, + SampleType, +) +from cg.constants.sequencing import DNA_PREP_CATEGORIES, SeqLibraryPrepCategory from cg.exc import CaseNotFoundError, CgError, OrderNotFoundError, SampleNotFoundError from cg.models.orders.constants import OrderType from cg.server.dto.samples.collaborator_samples_request import CollaboratorSamplesRequest @@ -1582,14 +1587,14 @@ def get_case_ids_for_samples(self, sample_ids: list[int]) -> list[str]: case_ids.extend(self.get_case_ids_with_sample(sample_id)) return list(set(case_ids)) - def get_related_samples( + def _get_related_samples_query( self, - sample_internal_id: str, + sample: Sample, prep_categories: list[SeqLibraryPrepCategory], collaborators: set[Customer], - ) -> list[Sample]: - """Returns a list of samples with the same subject_id, tumour status and within the collaborators of a given sample and within the given list of prep categories.""" - sample: Sample = self.get_sample_by_internal_id(internal_id=sample_internal_id) + ) -> 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.""" sample_application_version_query: Query = self._get_join_sample_application_version_query() @@ -1599,7 +1604,7 @@ def get_related_samples( filter_functions=[ApplicationFilter.BY_PREP_CATEGORIES], ) - sample_application_version_query: Query = apply_sample_filter( + samples: Query = apply_sample_filter( samples=sample_application_version_query, subject_id=sample.subject_id, is_tumour=sample.is_tumour, @@ -1610,27 +1615,45 @@ def get_related_samples( SampleFilter.BY_CUSTOMER_ENTRY_IDS, ], ) + return samples - return sample_application_version_query.all() + def get_uploaded_related_dna_cases(self, rna_case: Case) -> list[Case]: + """Returns all uploaded DNA cases ids related to the given RNA case.""" - def get_related_cases( - self, sample_internal_id: str, workflows: list[Workflow], collaborators: set[Customer] - ) -> list[Case]: - """Return a list of cases linked to the given sample within the given list of workflows and customers in a collaboration.""" + related_dna_cases: list[Case] = [] + for rna_sample in rna_case.samples: - cases_with_samples: Query = self._join_sample_and_case() - cases_with_samples: Query = apply_case_sample_filter( - case_samples=cases_with_samples, - sample_internal_id=sample_internal_id, - filter_functions=[CaseSampleFilter.CASES_WITH_SAMPLE_BY_INTERNAL_ID], - ) + collaborators: set[Customer] = rna_sample.customer.collaborators - return apply_case_filter( - cases=cases_with_samples, - workflows=workflows, - customer_entry_ids=[customer.id for customer in collaborators], - filter_functions=[ - CaseFilter.BY_WORKFLOWS, - CaseFilter.BY_CUSTOMER_ENTRY_IDS, - ], - ).all() + related_dna_samples_query: Query = self._get_related_samples_query( + sample=rna_sample, + prep_categories=DNA_PREP_CATEGORIES, + collaborators=collaborators, + ) + + dna_samples_cases_analysis_query: Query = ( + related_dna_samples_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.id for customer in collaborators], + 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() + ) + + related_dna_cases.extend([case for case in uploaded_dna_cases]) + + return related_dna_cases diff --git a/cg/store/filters/status_case_sample_filters.py b/cg/store/filters/status_case_sample_filters.py index 508b98435d..b8f30cec25 100644 --- a/cg/store/filters/status_case_sample_filters.py +++ b/cg/store/filters/status_case_sample_filters.py @@ -87,6 +87,7 @@ def apply_case_sample_filter( cases_to_exclude: list[str] = [], sample_entry_id: int | None = None, sample_internal_id: str | None = None, + sample_internal_ids: list[str] | None = None, order_id: int | None = None, ) -> Query: """Apply filtering functions to the sample queries and return filtered results.""" @@ -98,6 +99,7 @@ def apply_case_sample_filter( cases_to_exclude=cases_to_exclude, sample_entry_id=sample_entry_id, sample_internal_id=sample_internal_id, + sample_internal_ids=sample_internal_ids, order_id=order_id, ) return case_samples diff --git a/cg/store/filters/status_sample_filters.py b/cg/store/filters/status_sample_filters.py index 46fdfd57b7..cf9818c441 100644 --- a/cg/store/filters/status_sample_filters.py +++ b/cg/store/filters/status_sample_filters.py @@ -13,6 +13,11 @@ def filter_samples_by_internal_id(internal_id: str, samples: Query, **kwargs) -> return samples.filter(Sample.internal_id == internal_id) +def filter_samples_by_internal_ids(internal_ids: list[str], samples: Query, **kwargs) -> Query: + """Return sample by internal id.""" + return samples.filter(Sample.internal_id.in_(internal_ids)) if internal_ids else samples + + def filter_samples_by_name(name: str, samples: Query, **kwargs) -> Query: """Return sample with sample name.""" return samples.filter(Sample.name == name) @@ -224,6 +229,7 @@ class SampleFilter(Enum): BY_ENTRY_ID: Callable = filter_samples_by_entry_id BY_IDENTIFIER_NAME_AND_VALUE: Callable = filter_samples_by_identifier_name_and_value BY_INTERNAL_ID: Callable = filter_samples_by_internal_id + BY_INTERNAL_IDS: Callable = filter_samples_by_internal_ids BY_INTERNAL_ID_OR_NAME_SEARCH: Callable = filter_samples_by_internal_id_or_name_search BY_INTERNAL_ID_PATTERN: Callable = filter_samples_by_internal_id_pattern BY_INVOICE_ID: Callable = filter_samples_by_invoice_id diff --git a/tests/store/crud/conftest.py b/tests/store/crud/conftest.py index d71ab9c025..7cc7c61389 100644 --- a/tests/store/crud/conftest.py +++ b/tests/store/crud/conftest.py @@ -121,8 +121,14 @@ def store_with_samples_that_have_names(store: Store, helpers: StoreHelpers) -> S @pytest.fixture def store_with_rna_and_dna_samples_and_cases(store: Store, helpers: StoreHelpers) -> Store: - """Return a store with 1 rna sample 3 dna samples related to the rna sample and 1 more dna sample not related to the dna sample.""" - helpers.add_sample( + """Return a store with: + - 1 rna sample + - 3 dna samples related to the rna sample (with different prep categories) + - 1 more dna sample not related to the dna sample + - 2 dna cases including the related dna sample + - 1 dna case including the unrelated dna sample""" + + rna_sample: Sample = helpers.add_sample( store=store, internal_id="rna_sample", application_type=SeqLibraryPrepCategory.WHOLE_TRANSCRIPTOME_SEQUENCING.value, @@ -139,6 +145,7 @@ def store_with_rna_and_dna_samples_and_cases(store: Store, helpers: StoreHelpers is_tumour=True, customer_id="cust001", ) + helpers.add_sample( store=store, internal_id="related_dna_sample_2", @@ -157,6 +164,7 @@ def store_with_rna_and_dna_samples_and_cases(store: Store, helpers: StoreHelpers is_tumour=True, customer_id="cust000", ) + helpers.add_sample( store=store, internal_id="not_related_dna_sample", @@ -167,6 +175,15 @@ def store_with_rna_and_dna_samples_and_cases(store: Store, helpers: StoreHelpers customer_id="cust000", ) + rna_case: Case = helpers.add_case( + store=store, + internal_id="rna_case", + name="rna_case", + data_analysis=Workflow.MIP_RNA, + customer_id="cust000", + ) + helpers.add_relationship(store=store, sample=rna_sample, case=rna_case) + related_dna_case_1: Case = helpers.add_case( store=store, internal_id="related_dna_case_1", @@ -174,6 +191,7 @@ def store_with_rna_and_dna_samples_and_cases(store: Store, helpers: StoreHelpers customer_id="cust001", ) helpers.add_relationship(store=store, sample=related_dna_sample_1, case=related_dna_case_1) + helpers.add_analysis(store=store, case=related_dna_case_1, uploaded_at=datetime.now()) related_dna_case_2: Case = helpers.add_case( store=store, @@ -208,16 +226,18 @@ def rna_sample_collaborators(rna_sample: Sample) -> set[Customer]: @pytest.fixture -def related_dna_sample_1(store_with_rna_and_dna_samples_and_cases: Store) -> Sample: - return store_with_rna_and_dna_samples_and_cases.get_sample_by_internal_id( - internal_id="related_dna_sample_1" - ) +def rna_case(store_with_rna_and_dna_samples_and_cases: Store) -> Case: + return store_with_rna_and_dna_samples_and_cases.get_case_by_internal_id("rna_case") @pytest.fixture -def related_dna_samples( - store_with_rna_and_dna_samples_and_cases: Store, related_dna_sample_1: Sample -) -> list[Sample]: +def related_dna_samples(store_with_rna_and_dna_samples_and_cases: Store) -> list[Sample]: + related_dna_sample_1: Sample = ( + store_with_rna_and_dna_samples_and_cases.get_sample_by_internal_id( + internal_id="related_dna_sample_1" + ) + ) + related_dna_sample_2: Sample = ( store_with_rna_and_dna_samples_and_cases.get_sample_by_internal_id( internal_id="related_dna_sample_2" @@ -247,6 +267,15 @@ def related_dna_cases(store_with_rna_and_dna_samples_and_cases: Store) -> list[C return [related_dna_case_1, related_dna_case_2] +@pytest.fixture +def uploaded_related_dna_case(related_dna_cases: list[Case]) -> list[Case]: + related_uploaded_dna_cases: list[Case] = [] + for case in related_dna_cases: + if case.is_uploaded: + related_uploaded_dna_cases.append(case) + return related_uploaded_dna_cases + + @pytest.fixture def store_with_active_sample_analyze( store: Store, helpers: StoreHelpers diff --git a/tests/store/crud/read/test_read_case.py b/tests/store/crud/read/test_read_case.py index 7970357cdf..c306e92cb6 100644 --- a/tests/store/crud/read/test_read_case.py +++ b/tests/store/crud/read/test_read_case.py @@ -1,31 +1,22 @@ -from cg.constants import Workflow -from cg.store.models import Case, Customer, Sample +from cg.store.models import Case from cg.store.store import Store -def test_get_related_cases( +def test_get_uploaded_related_dna_case( store_with_rna_and_dna_samples_and_cases: Store, - related_dna_sample_1: Sample, - rna_sample_collaborators: set[Customer], + rna_case: Case, + uploaded_related_dna_case: list[Case], related_dna_cases: list[Case], ): - # GIVEN a database with a sample in several cases - # GIVEN a list of workflows + # GIVEN a database with an RNA case and several related DNA cases + # GIVEN that some of the DNA cases are uploaded and others not + store: Store = store_with_rna_and_dna_samples_and_cases - workflows = [ - Workflow.MIP_DNA, - Workflow.BALSAMIC, - Workflow.BALSAMIC_UMI, - ] - - # WHEN getting the cases from the given sample by the given workflows and within the given collaborators - fetched_related_dna_cases: list[Case] = ( - store_with_rna_and_dna_samples_and_cases.get_related_cases( - sample_internal_id=related_dna_sample_1.internal_id, - workflows=workflows, - collaborators=rna_sample_collaborators, - ) + # WHEN getting the related DNA cases that are uploaded + fetched_uploaded_related_dna_case: list[Case] = store.get_uploaded_related_dna_cases( + rna_case=rna_case, ) # THEN the correct set of cases is returned - assert set(related_dna_cases) == set(fetched_related_dna_cases) + assert set(fetched_uploaded_related_dna_case) == set(uploaded_related_dna_case) + assert set(fetched_uploaded_related_dna_case) != set(related_dna_cases) diff --git a/tests/store/crud/read/test_read_sample.py b/tests/store/crud/read/test_read_sample.py index 9af8e375dc..e2ce710ba1 100644 --- a/tests/store/crud/read/test_read_sample.py +++ b/tests/store/crud/read/test_read_sample.py @@ -6,7 +6,7 @@ from _pytest.fixtures import FixtureRequest from sqlalchemy.orm import Query -from cg.constants.sequencing import SeqLibraryPrepCategory +from cg.constants.sequencing import DNA_PREP_CATEGORIES, SeqLibraryPrepCategory from cg.store.models import Customer, Invoice, Sample from cg.store.store import Store from tests.store_helpers import StoreHelpers @@ -614,18 +614,17 @@ def test_get_related_samples( # GIVEN a database with an RNA sample and several DNA samples with the same subject_id and tumour status as the given sample # GIVEN that all customers are in a collaboration # GIVEN a list of dna prep categories - dna_prep_categories: list[SeqLibraryPrepCategory] = [ - SeqLibraryPrepCategory.WHOLE_GENOME_SEQUENCING, - SeqLibraryPrepCategory.TARGETED_GENOME_SEQUENCING, - SeqLibraryPrepCategory.WHOLE_EXOME_SEQUENCING, - ] + store: Store = store_with_rna_and_dna_samples_and_cases + prep_categories: list[SeqLibraryPrepCategory] = DNA_PREP_CATEGORIES # WHEN getting the related DNA samples to the given sample - fetched_related_dna_samples = store_with_rna_and_dna_samples_and_cases.get_related_samples( - sample_internal_id=rna_sample.internal_id, - prep_categories=dna_prep_categories, + fetched_related_dna_samples_query = store._get_related_samples_query( + sample=rna_sample, + prep_categories=prep_categories, collaborators=rna_sample_collaborators, ) + fetched_related_dna_samples: list[Sample] = fetched_related_dna_samples_query.all() + # THEN the correct set of samples is returned assert set(related_dna_samples) == set(fetched_related_dna_samples) From ee779fadbce94a7cd9347f9f79369ab787da1712 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Thu, 28 Nov 2024 14:58:54 +0000 Subject: [PATCH 13/25] =?UTF-8?q?Bump=20version:=2064.5.17=20=E2=86=92=206?= =?UTF-8?q?4.5.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 c3cd96cb33..cd1c937392 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 64.5.17 +current_version = 64.5.18 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index 427a1019d5..45857a91b5 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "64.5.17" +__version__ = "64.5.18" diff --git a/pyproject.toml b/pyproject.toml index 5efde72096..9a5d17ccab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "cg" -version = "64.5.17" +version = "64.5.18" description = "Clinical Genomics command center" authors = ["Clinical Genomics "] readme = "README.md" From 41db2543e99a564db737fe1e378fc653953923e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20Ohlsson=20=C3=85ngnell?= <40887124+islean@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:07:28 +0100 Subject: [PATCH 14/25] Speed up sample edits (#3984) (patch) ### Fixed - sequencing metrics are excluded from the edit sample flask form --- cg/server/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cg/server/admin.py b/cg/server/admin.py index 6fa37ae6f7..6e981f7a41 100644 --- a/cg/server/admin.py +++ b/cg/server/admin.py @@ -631,6 +631,7 @@ class SampleView(BaseView): "_phenotype_terms", "links", "mother_links", + "sequencing_metrics", ] @staticmethod From 8d2916cde4071c2f089241330c8113e11d5dcdcf Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Thu, 28 Nov 2024 15:07:55 +0000 Subject: [PATCH 15/25] =?UTF-8?q?Bump=20version:=2064.5.18=20=E2=86=92=206?= =?UTF-8?q?4.5.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 cd1c937392..799ad6a2cd 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 64.5.18 +current_version = 64.5.19 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index 45857a91b5..e808d82cc1 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "64.5.18" +__version__ = "64.5.19" diff --git a/pyproject.toml b/pyproject.toml index 9a5d17ccab..f816d91d16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "cg" -version = "64.5.18" +version = "64.5.19" description = "Clinical Genomics command center" authors = ["Clinical Genomics "] readme = "README.md" From 075f5fe929984d23f7269926616b645b656ddd97 Mon Sep 17 00:00:00 2001 From: Henrik Stranneheim Date: Mon, 2 Dec 2024 08:54:24 +0100 Subject: [PATCH 16/25] Refactor analysistype 3 (#3982) ### Changed - Removed AnalysisType from constants.py - Moved AnalysisType to tb.py - Renamed AnalysisTypes to AnalysisType --- cg/apps/demultiplex/demultiplex_api.py | 4 +-- cg/cli/workflow/raw_data/raw_data_service.py | 4 +-- cg/constants/constants.py | 8 ----- cg/constants/tb.py | 2 +- cg/meta/delivery_report/balsamic.py | 6 ++-- .../upload/scout/balsamic_config_builder.py | 9 ++---- cg/meta/workflow/analysis.py | 29 ++++++++++--------- cg/meta/workflow/balsamic.py | 6 ++-- cg/meta/workflow/mip_dna.py | 10 +++---- cg/meta/workflow/raredisease.py | 7 +++-- .../deliver_files_service.py | 4 +-- cg/services/deliver_files/rsync/service.py | 9 +++--- tests/conftest.py | 4 +-- .../delivery_report/test_models_validators.py | 7 +++-- 14 files changed, 47 insertions(+), 62 deletions(-) diff --git a/cg/apps/demultiplex/demultiplex_api.py b/cg/apps/demultiplex/demultiplex_api.py index e1eaadabf3..c77d86533b 100644 --- a/cg/apps/demultiplex/demultiplex_api.py +++ b/cg/apps/demultiplex/demultiplex_api.py @@ -13,7 +13,7 @@ from cg.constants.constants import FileFormat, Workflow from cg.constants.demultiplexing import DemultiplexingDirsAndFiles from cg.constants.priority import SlurmQos -from cg.constants.tb import AnalysisTypes +from cg.constants.tb import AnalysisType from cg.exc import HousekeeperFileMissingError from cg.io.controller import WriteFile from cg.models.demultiplex.sbatch import SbatchCommand, SbatchError @@ -210,7 +210,7 @@ def add_to_trailblazer( ) tb_api.add_pending_analysis( case_id=sequencing_run.id, - analysis_type=AnalysisTypes.OTHER, + analysis_type=AnalysisType.OTHER, config_path=sequencing_run.trailblazer_config_path.as_posix(), out_dir=sequencing_run.trailblazer_config_path.parent.as_posix(), slurm_quality_of_service=self.slurm_quality_of_service, diff --git a/cg/cli/workflow/raw_data/raw_data_service.py b/cg/cli/workflow/raw_data/raw_data_service.py index 55685b8ec3..f4d4b49ec4 100644 --- a/cg/cli/workflow/raw_data/raw_data_service.py +++ b/cg/cli/workflow/raw_data/raw_data_service.py @@ -1,8 +1,8 @@ import datetime as dt from cg.apps.tb.api import TrailblazerAPI -from cg.constants.constants import AnalysisType, Workflow -from cg.constants.tb import AnalysisStatus +from cg.constants.constants import Workflow +from cg.constants.tb import AnalysisStatus, AnalysisType from cg.exc import CaseNotFoundError from cg.store.models import Analysis, Case from cg.store.store import Store diff --git a/cg/constants/constants.py b/cg/constants/constants.py index 8a8e573b6d..175b9c152a 100644 --- a/cg/constants/constants.py +++ b/cg/constants/constants.py @@ -86,14 +86,6 @@ def statuses(cls) -> list[str]: return list(map(lambda status: status.value, cls)) -class AnalysisType(StrEnum): - TARGETED_GENOME_SEQUENCING: str = "tgs" - WHOLE_EXOME_SEQUENCING: str = "wes" - WHOLE_GENOME_SEQUENCING: str = "wgs" - WHOLE_TRANSCRIPTOME_SEQUENCING: str = "wts" - OTHER: str = "other" - - class CancerAnalysisType(StrEnum): TUMOR_NORMAL = auto() TUMOR_NORMAL_PANEL = auto() diff --git a/cg/constants/tb.py b/cg/constants/tb.py index bf8768244d..cec5bb528d 100644 --- a/cg/constants/tb.py +++ b/cg/constants/tb.py @@ -14,7 +14,7 @@ class AnalysisStatus: QC: str = "qc" -class AnalysisTypes(StrEnum): +class AnalysisType(StrEnum): OTHER: str = "other" TGS: str = SeqLibraryPrepCategory.TARGETED_GENOME_SEQUENCING WES: str = SeqLibraryPrepCategory.WHOLE_EXOME_SEQUENCING diff --git a/cg/meta/delivery_report/balsamic.py b/cg/meta/delivery_report/balsamic.py index 238d53ae28..dacc61b1d8 100644 --- a/cg/meta/delivery_report/balsamic.py +++ b/cg/meta/delivery_report/balsamic.py @@ -17,8 +17,8 @@ REQUIRED_SAMPLE_TIMESTAMP_FIELDS, Workflow, ) -from cg.constants.constants import AnalysisType from cg.constants.scout import ScoutUploadKey +from cg.constants.tb import AnalysisType from cg.meta.delivery_report.data_validators import get_million_read_pairs from cg.meta.delivery_report.delivery_report_api import DeliveryReportAPI from cg.meta.workflow.balsamic import BalsamicAnalysisAPI @@ -52,9 +52,7 @@ def get_sample_metadata( passed_initial_qc: bool | None = self.lims_api.has_sample_passed_initial_qc( sample.internal_id ) - if AnalysisType.WHOLE_GENOME_SEQUENCING in self.analysis_api.get_data_analysis_type( - case.internal_id - ): + if AnalysisType.WGS in self.analysis_api.get_data_analysis_type(case.internal_id): return self.get_wgs_metadata( million_read_pairs=million_read_pairs, passed_initial_qc=passed_initial_qc, diff --git a/cg/meta/upload/scout/balsamic_config_builder.py b/cg/meta/upload/scout/balsamic_config_builder.py index 61f8a34b3a..ac90858420 100644 --- a/cg/meta/upload/scout/balsamic_config_builder.py +++ b/cg/meta/upload/scout/balsamic_config_builder.py @@ -5,12 +5,7 @@ from cg.apps.lims import LimsAPI from cg.constants.constants import SampleType from cg.constants.housekeeper_tags import HK_DELIVERY_REPORT_TAG -from cg.constants.scout import ( - BALSAMIC_CASE_TAGS, - BALSAMIC_SAMPLE_TAGS, - GenomeBuild, - UploadTrack, -) +from cg.constants.scout import BALSAMIC_CASE_TAGS, BALSAMIC_SAMPLE_TAGS, GenomeBuild, UploadTrack from cg.constants.subject import PhenotypeStatus from cg.meta.upload.scout.hk_tags import CaseTags, SampleTags from cg.meta.upload.scout.scout_config_builder import ScoutConfigBuilder @@ -71,7 +66,7 @@ def build_config_sample(self, case_sample: CaseSample) -> ScoutCancerIndividual: def get_balsamic_analysis_type(self, sample: Sample) -> str: """Returns a formatted balsamic analysis type""" - analysis_type: str = BalsamicAnalysisAPI.get_application_type(sample_obj=sample) + analysis_type: str = BalsamicAnalysisAPI.get_analysis_type(sample=sample) if analysis_type == "tgs": analysis_type = "panel" if analysis_type == "wgs": diff --git a/cg/meta/workflow/analysis.py b/cg/meta/workflow/analysis.py index b650383b7e..204022a3b2 100644 --- a/cg/meta/workflow/analysis.py +++ b/cg/meta/workflow/analysis.py @@ -13,7 +13,6 @@ from cg.clients.chanjo2.models import CoverageMetrics from cg.constants import EXIT_FAIL, EXIT_SUCCESS, Priority, SequencingFileTag, Workflow from cg.constants.constants import ( - AnalysisType, CaseActions, CustomerId, FileFormat, @@ -23,7 +22,8 @@ from cg.constants.gene_panel import GenePanelCombo, GenePanelMasterList from cg.constants.priority import SlurmQos from cg.constants.scout import HGNC_ID, ScoutExportFileName -from cg.constants.tb import AnalysisStatus +from cg.constants.sequencing import SeqLibraryPrepCategory +from cg.constants.tb import AnalysisStatus, AnalysisType from cg.exc import AnalysisNotReadyError, BundleAlreadyAddedError, CgDataError, CgError from cg.io.controller import WriteFile from cg.meta.archive.archive import SpringArchiveAPI @@ -168,17 +168,18 @@ def get_bundle_deliverables_type(self, case_id: str) -> str | None: return None @staticmethod - def get_application_type(sample_obj: Sample) -> str: + def get_analysis_type(sample: Sample) -> str: """ - Gets application type for sample. Only application types supported by trailblazer (or other) + Return the analysis type for sample. + Only analysis types supported by Trailblazer are valid outputs. """ - prep_category: str = sample_obj.prep_category + prep_category: str = sample.prep_category if prep_category and prep_category.lower() in { - AnalysisType.TARGETED_GENOME_SEQUENCING, - AnalysisType.WHOLE_EXOME_SEQUENCING, - AnalysisType.WHOLE_GENOME_SEQUENCING, - AnalysisType.WHOLE_TRANSCRIPTOME_SEQUENCING, + SeqLibraryPrepCategory.TARGETED_GENOME_SEQUENCING, + SeqLibraryPrepCategory.WHOLE_EXOME_SEQUENCING, + SeqLibraryPrepCategory.WHOLE_GENOME_SEQUENCING, + SeqLibraryPrepCategory.WHOLE_TRANSCRIPTOME_SEQUENCING, }: return prep_category.lower() return AnalysisType.OTHER @@ -186,7 +187,7 @@ def get_application_type(sample_obj: Sample) -> str: def get_case_application_type(self, case_id: str) -> str: """Returns the application type for samples in a case.""" samples: list[Sample] = self.status_db.get_samples_by_case_id(case_id) - application_types: set[str] = {self.get_application_type(sample) for sample in samples} + application_types: set[str] = {self.get_analysis_type(sample) for sample in samples} if len(application_types) > 1: raise CgError( @@ -197,7 +198,7 @@ def get_case_application_type(self, case_id: str) -> str: def are_case_samples_rna(self, case_id: str) -> bool: analysis_type: str = self.get_case_application_type(case_id) - return analysis_type == AnalysisType.WHOLE_TRANSCRIPTOME_SEQUENCING + return analysis_type == AnalysisType.WTS def get_case_source_type(self, case_id: str) -> str | None: """ @@ -217,7 +218,7 @@ def get_case_source_type(self, case_id: str) -> str | None: def has_case_only_exome_samples(self, case_id: str) -> bool: """Returns True if the application type for all samples in a case is WHOLE_EXOME_SEQUENCING.""" application_type: str = self.get_case_application_type(case_id) - return application_type == AnalysisType.WHOLE_EXOME_SEQUENCING + return application_type == AnalysisType.WES def upload_bundle_housekeeper( self, case_id: str, dry_run: bool = False, force: bool = False @@ -282,7 +283,7 @@ def add_pending_trailblazer_analysis( tower_workflow_id: str | None = None, ) -> None: self.check_analysis_ongoing(case_id) - application_type: str = self.get_application_type( + analysis_type: str = self.get_analysis_type( self.status_db.get_case_by_internal_id(case_id).links[0].sample ) config_path: str = self.get_job_ids_path(case_id).as_posix() @@ -295,7 +296,7 @@ def add_pending_trailblazer_analysis( workflow_manager: str = self.get_workflow_manager() is_case_for_development: bool = self._is_case_for_development(case_id) self.trailblazer_api.add_pending_analysis( - analysis_type=application_type, + analysis_type=analysis_type, case_id=case_id, config_path=config_path, email=email, diff --git a/cg/meta/workflow/balsamic.py b/cg/meta/workflow/balsamic.py index 2c1b465db2..bbef5c0faf 100644 --- a/cg/meta/workflow/balsamic.py +++ b/cg/meta/workflow/balsamic.py @@ -134,7 +134,7 @@ def get_bundle_deliverables_type(self, case_id: str) -> str: self.status_db.get_case_by_internal_id(internal_id=case_id).links ) - application_type: str = self.get_application_type( + application_type: str = self.get_analysis_type( self.status_db.get_case_by_internal_id(internal_id=case_id).links[0].sample ) sample_type = "tumor" @@ -500,7 +500,7 @@ def get_sample_params(self, case_id: str, panel_bed: str | None) -> dict: link_object.sample.internal_id: { "sex": link_object.sample.sex, "tissue_type": self.get_sample_type(link_object.sample).value, - "application_type": self.get_application_type(link_object.sample), + "application_type": self.get_analysis_type(link_object.sample), "target_bed": self.resolve_target_bed(panel_bed=panel_bed, link_object=link_object), } for link_object in self.status_db.get_case_by_internal_id(internal_id=case_id).links @@ -512,7 +512,7 @@ def get_sample_params(self, case_id: str, panel_bed: str | None) -> dict: def resolve_target_bed(self, panel_bed: str | None, link_object: CaseSample) -> str | None: if panel_bed: return panel_bed - if self.get_application_type(link_object.sample) not in self.__BALSAMIC_BED_APPLICATIONS: + if self.get_analysis_type(link_object.sample) not in self.__BALSAMIC_BED_APPLICATIONS: return None return self.get_target_bed_from_lims(link_object.case.internal_id) diff --git a/cg/meta/workflow/mip_dna.py b/cg/meta/workflow/mip_dna.py index c5090509d7..88961c1734 100644 --- a/cg/meta/workflow/mip_dna.py +++ b/cg/meta/workflow/mip_dna.py @@ -1,10 +1,10 @@ import logging from cg.constants import DEFAULT_CAPTURE_KIT, Workflow -from cg.constants.constants import AnalysisType from cg.constants.gene_panel import GENOME_BUILD_37 from cg.constants.pedigree import Pedigree from cg.constants.scout import MIP_CASE_TAGS +from cg.constants.tb import AnalysisType from cg.meta.workflow.mip import MipAnalysisAPI from cg.models.cg_config import CGConfig from cg.models.mip.mip_analysis import MipAnalysis @@ -54,7 +54,7 @@ def config_sample( ) -> dict[str, str | int | None]: """Return config sample data.""" sample_data: dict[str, str | int] = self.get_sample_data(link_obj=link_obj) - if sample_data["analysis_type"] == AnalysisType.WHOLE_GENOME_SEQUENCING: + if sample_data["analysis_type"] == AnalysisType.WGS: sample_data["capture_kit"]: str = panel_bed or DEFAULT_CAPTURE_KIT else: sample_data["capture_kit"]: str | None = panel_bed or self.get_target_bed_from_lims( @@ -90,10 +90,8 @@ def get_data_analysis_type(self, case_id: str) -> str: link.sample.application_version.application.analysis_type for link in case.links } if len(analysis_types) > 1: - LOG.warning( - f"Multiple analysis types found. Defaulting to {AnalysisType.WHOLE_GENOME_SEQUENCING}." - ) - return AnalysisType.WHOLE_GENOME_SEQUENCING + LOG.warning(f"Multiple analysis types found. Defaulting to {AnalysisType.WGS}.") + return AnalysisType.WGS return analysis_types.pop() if analysis_types else None def get_scout_upload_case_tags(self) -> dict: diff --git a/cg/meta/workflow/raredisease.py b/cg/meta/workflow/raredisease.py index 35186e082a..adc371616b 100644 --- a/cg/meta/workflow/raredisease.py +++ b/cg/meta/workflow/raredisease.py @@ -14,7 +14,7 @@ CoverageSample, ) from cg.constants import DEFAULT_CAPTURE_KIT, Workflow -from cg.constants.constants import AnalysisType, GenomeVersion +from cg.constants.constants import GenomeVersion from cg.constants.nf_analysis import ( RAREDISEASE_COVERAGE_FILE_TAGS, RAREDISEASE_COVERAGE_INTERVAL_TYPE, @@ -24,6 +24,7 @@ ) from cg.constants.scout import RAREDISEASE_CASE_TAGS, ScoutExportFileName from cg.constants.subject import PlinkPhenotypeStatus, PlinkSex +from cg.constants.tb import AnalysisType from cg.meta.workflow.nf_analysis import NfAnalysisAPI from cg.models.cg_config import CGConfig from cg.models.deliverables.metric_deliverables import MetricsBase, MultiqcDataJson @@ -98,13 +99,13 @@ def get_target_bed(self, case_id: str, analysis_type: str) -> str: """ target_bed_file: str = self.get_target_bed_from_lims(case_id=case_id) if not target_bed_file: - if analysis_type == AnalysisType.WHOLE_GENOME_SEQUENCING: + if analysis_type == AnalysisType.WGS: return DEFAULT_CAPTURE_KIT raise ValueError("No capture kit was found in LIMS") return target_bed_file def get_germlinecnvcaller_flag(self, analysis_type: str) -> bool: - if analysis_type == AnalysisType.WHOLE_GENOME_SEQUENCING: + if analysis_type == AnalysisType.WGS: return True return False diff --git a/cg/services/deliver_files/deliver_files_service/deliver_files_service.py b/cg/services/deliver_files/deliver_files_service/deliver_files_service.py index 6b45433885..a1aea36ddd 100644 --- a/cg/services/deliver_files/deliver_files_service/deliver_files_service.py +++ b/cg/services/deliver_files/deliver_files_service/deliver_files_service.py @@ -4,7 +4,7 @@ from cg.apps.tb import TrailblazerAPI from cg.apps.tb.models import TrailblazerAnalysis from cg.constants import Priority, Workflow -from cg.constants.tb import AnalysisTypes +from cg.constants.tb import AnalysisType from cg.services.analysis_service.analysis_service import AnalysisService from cg.services.deliver_files.deliver_files_service.error_handling import ( handle_no_delivery_files_error, @@ -128,7 +128,7 @@ def _add_trailblazer_tracking(self, case: Case, job_id: int, dry_run: bool) -> N LOG.debug(f"[TB SERVICE] Adding analysis for case {case.internal_id} to Trailblazer") analysis: TrailblazerAnalysis = self.tb_service.add_pending_analysis( case_id=f"{case.internal_id}_rsync", - analysis_type=AnalysisTypes.OTHER, + analysis_type=AnalysisType.OTHER, config_path=self.rsync_service.trailblazer_config_path.as_posix(), order_id=case.latest_order.id, out_dir=self.rsync_service.log_dir.as_posix(), diff --git a/cg/services/deliver_files/rsync/service.py b/cg/services/deliver_files/rsync/service.py index 3fa6194e46..74bdae00e0 100644 --- a/cg/services/deliver_files/rsync/service.py +++ b/cg/services/deliver_files/rsync/service.py @@ -12,18 +12,17 @@ from cg.constants.constants import FileFormat from cg.constants.delivery import INBOX_NAME from cg.constants.priority import SlurmAccount, SlurmQos -from cg.constants.tb import AnalysisTypes +from cg.constants.tb import AnalysisType from cg.exc import CgError from cg.io.controller import WriteFile +from cg.models.slurm.sbatch import Sbatch from cg.services.deliver_files.rsync.models import RsyncDeliveryConfig from cg.services.deliver_files.rsync.sbatch import ( + COVID_REPORT_RSYNC, COVID_RSYNC, ERROR_RSYNC_FUNCTION, RSYNC_COMMAND, - COVID_REPORT_RSYNC, ) - -from cg.models.slurm.sbatch import Sbatch from cg.store.models import Case from cg.store.store import Store @@ -159,7 +158,7 @@ def add_to_trailblazer_api( ) tb_api.add_pending_analysis( case_id=ticket, - analysis_type=AnalysisTypes.OTHER, + analysis_type=AnalysisType.OTHER, config_path=self.trailblazer_config_path.as_posix(), out_dir=self.log_dir.as_posix(), slurm_quality_of_service=self.slurm_quality_of_service, diff --git a/tests/conftest.py b/tests/conftest.py index 03206c4271..5350c0438f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,7 +37,7 @@ from cg.constants.scout import ScoutExportFileName from cg.constants.sequencing import SequencingPlatform from cg.constants.subject import Sex -from cg.constants.tb import AnalysisTypes +from cg.constants.tb import AnalysisType from cg.io.controller import ReadFile, WriteFile from cg.io.json import read_json, write_json from cg.io.yaml import read_yaml, write_yaml @@ -2581,7 +2581,7 @@ def raredisease_parameters_default( outdir=Path(raredisease_dir, raredisease_case_id), target_bed_file=bed_version_file_name, skip_germlinecnvcaller=False, - analysis_type=AnalysisTypes.WES, + analysis_type=AnalysisType.WES, save_mapped_as_cram=True, vcfanno_extra_resources=str( Path(raredisease_dir, raredisease_case_id + ScoutExportFileName.MANAGED_VARIANTS) diff --git a/tests/models/delivery_report/test_models_validators.py b/tests/models/delivery_report/test_models_validators.py index 16c620df7e..cc0f62816b 100644 --- a/tests/models/delivery_report/test_models_validators.py +++ b/tests/models/delivery_report/test_models_validators.py @@ -19,7 +19,8 @@ YES_FIELD, Sex, ) -from cg.constants.constants import AnalysisType, Workflow +from cg.constants.constants import Workflow +from cg.constants.tb import AnalysisType from cg.meta.delivery_report.delivery_report_api import DeliveryReportAPI from cg.meta.delivery_report.rnafusion import RnafusionDeliveryReportAPI from cg.models.analysis import NextflowAnalysis @@ -94,7 +95,7 @@ def test_get_number_as_string(input_value: Any, expected_output: str, caplog: Lo # GIVEN a list of number inputs and their expected values - if expected_output == ValueError: + if expected_output is ValueError: # WHEN getting a string representation of a number with pytest.raises(ValueError): get_number_as_string(input_value) @@ -221,7 +222,7 @@ def test_get_analysis_type_as_string(): """Test analysis type formatting for the delivery report generation.""" # GIVEN a WHOLE_GENOME_SEQUENCING analysis type and a model info dictionary - analysis_type: str = AnalysisType.WHOLE_GENOME_SEQUENCING + analysis_type: str = AnalysisType.WGS model_info = ValidationInfo model_info.data = {"workflow": Workflow.MIP_DNA.value} From d6f28883a5b49565c360283d714a1ca060cefd7d Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Mon, 2 Dec 2024 07:54:51 +0000 Subject: [PATCH 17/25] =?UTF-8?q?Bump=20version:=2064.5.19=20=E2=86=92=206?= =?UTF-8?q?4.5.20=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 799ad6a2cd..d843a8783c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 64.5.19 +current_version = 64.5.20 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index e808d82cc1..c6460a2c96 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "64.5.19" +__version__ = "64.5.20" diff --git a/pyproject.toml b/pyproject.toml index f816d91d16..1cd39176dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "cg" -version = "64.5.19" +version = "64.5.20" description = "Clinical Genomics command center" authors = ["Clinical Genomics "] readme = "README.md" From cfe22dbb2007289a29a9f9013ca84474efc40c86 Mon Sep 17 00:00:00 2001 From: Peter Pruisscher <57712924+peterpru@users.noreply.github.com> Date: Tue, 3 Dec 2024 06:28:02 +0100 Subject: [PATCH 18/25] Add nallo to analysis options (#3989) ### Added - add Nallo to analysis options --- ...2c02a4966_add_nallo_to_analysis_options.py | 105 ++++++++++++++++++ cg/constants/constants.py | 1 + 2 files changed, 106 insertions(+) create mode 100644 alembic/versions/2024_12_02_5552c02a4966_add_nallo_to_analysis_options.py diff --git a/alembic/versions/2024_12_02_5552c02a4966_add_nallo_to_analysis_options.py b/alembic/versions/2024_12_02_5552c02a4966_add_nallo_to_analysis_options.py new file mode 100644 index 0000000000..edb6c11135 --- /dev/null +++ b/alembic/versions/2024_12_02_5552c02a4966_add_nallo_to_analysis_options.py @@ -0,0 +1,105 @@ +"""add-nallo-to-analysis-options + +Revision ID: 5552c02a4966 +Revises: 05ffb5e13d7b +Create Date: 2024-12-02 11:35:31.725343 + +""" + +from enum import StrEnum + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + +# revision identifiers, used by Alembic. +revision = "5552c02a4966" +down_revision = "05ffb5e13d7b" +branch_labels = None +depends_on = None + +base_options = ( + "balsamic", + "balsamic-pon", + "balsamic-qc", + "balsamic-umi", + "demultiplex", + "raw-data", + "fluffy", + "microsalt", + "mip-dna", + "mip-rna", + "mutant", + "raredisease", + "rnafusion", + "rsync", + "spring", + "taxprofiler", + "tomte", + "jasen", +) + +old_options = sorted(base_options) +new_options = sorted(base_options + ("nallo",)) + +old_analysis_enum = mysql.ENUM(*old_options) +new_analysis_enum = mysql.ENUM(*new_options) + + +class Pipeline(StrEnum): + BALSAMIC: str = "balsamic" + BALSAMIC_PON: str = "balsamic-pon" + BALSAMIC_QC: str = "balsamic-qc" + BALSAMIC_UMI: str = "balsamic-umi" + DEMULTIPLEX: str = "demultiplex" + FLUFFY: str = "fluffy" + JASEN: str = "jasen" + MICROSALT: str = "microsalt" + MIP_DNA: str = "mip-dna" + MIP_RNA: str = "mip-rna" + MUTANT: str = "mutant" + NALLO: str = "nallo" + RAREDISEASE: str = "raredisease" + RAW_DATA: str = "raw-data" + RNAFUSION: str = "rnafusion" + RSYNC: str = "rsync" + SPRING: str = "spring" + TAXPROFILER: str = "taxprofiler" + TOMTE: str = "tomte" + + +class Base(DeclarativeBase): + pass + + +class Analysis(Base): + __tablename__ = "analysis" + id = sa.Column(sa.types.Integer, primary_key=True) + workflow = sa.Column(sa.types.Enum(*list(Pipeline))) + + +class Case(Base): + __tablename__ = "case" + id = sa.Column(sa.types.Integer, primary_key=True) + data_analysis = sa.Column(sa.types.Enum(*list(Pipeline))) + internal_id = sa.Column(sa.types.String) + + +def upgrade(): + op.alter_column("case", "data_analysis", type_=new_analysis_enum) + op.alter_column("analysis", "workflow", type_=new_analysis_enum) + + +def downgrade(): + bind = op.get_bind() + session = sa.orm.Session(bind=bind) + for analysis in session.query(Analysis).filter(Analysis.workflow == "nallo"): + print(f"Changing pipeline for Case {Case.internal_id} to raw-data") + analysis.workflow = "raw-data" + for case in session.query(Case).filter(Case.data_analysis == "nallo"): + print(f"Changing data_analysis for Case {case.internal_id} to raw-data") + case.data_analysis = "raw-data" + op.alter_column("case", "data_analysis", type_=old_analysis_enum) + op.alter_column("analysis", "workflow", type_=old_analysis_enum) + session.commit() diff --git a/cg/constants/constants.py b/cg/constants/constants.py index 175b9c152a..c2dd3e5842 100644 --- a/cg/constants/constants.py +++ b/cg/constants/constants.py @@ -123,6 +123,7 @@ class Workflow(StrEnum): MIP_DNA: str = "mip-dna" MIP_RNA: str = "mip-rna" MUTANT: str = "mutant" + NALLO: str = "nallo" RAREDISEASE: str = "raredisease" RAW_DATA: str = "raw-data" RNAFUSION: str = "rnafusion" From 8108c6ac55b93f8cda7bdbf24e419095512ceeff Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Tue, 3 Dec 2024 05:28:29 +0000 Subject: [PATCH 19/25] =?UTF-8?q?Bump=20version:=2064.5.20=20=E2=86=92=206?= =?UTF-8?q?4.5.21=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 d843a8783c..8e1a37bf49 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 64.5.20 +current_version = 64.5.21 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index c6460a2c96..c5330b791b 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "64.5.20" +__version__ = "64.5.21" diff --git a/pyproject.toml b/pyproject.toml index 1cd39176dd..e659d97411 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "cg" -version = "64.5.20" +version = "64.5.21" description = "Clinical Genomics command center" authors = ["Clinical Genomics "] readme = "README.md" From ecc06d31ad248eb2fe819f3407296e659cdcf907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20Ohlsson=20=C3=85ngnell?= <40887124+islean@users.noreply.github.com> Date: Tue, 3 Dec 2024 13:28:48 +0100 Subject: [PATCH 20/25] Patch case addition to order form (#3990) (patch) ### Fixed - The cases field in the response from the order form parser is only populated for relevant order types --- cg/apps/orderform/orderform_parser.py | 8 +++++--- cg/apps/orderform/utils.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/cg/apps/orderform/orderform_parser.py b/cg/apps/orderform/orderform_parser.py index 9e84f72fe4..48e52bd6de 100644 --- a/cg/apps/orderform/orderform_parser.py +++ b/cg/apps/orderform/orderform_parser.py @@ -4,6 +4,7 @@ from pydantic import BaseModel, ConfigDict, constr +from cg.apps.orderform.utils import ORDER_TYPES_WITH_CASES from cg.constants import DataDelivery from cg.exc import OrderFormError from cg.models.orders.order import OrderType @@ -142,10 +143,11 @@ def expand_case(case_id: str, case_samples: list[OrderSample]) -> OrderCase: def generate_orderform(self) -> Orderform: """Generate an orderform""" - cases_map: dict[str, list[OrderSample]] = self.group_cases() case_objs: list[OrderCase] = [] - for case_id in cases_map: - case_objs.append(self.expand_case(case_id=case_id, case_samples=cases_map[case_id])) + if self.project_type in ORDER_TYPES_WITH_CASES: + cases_map: dict[str, list[OrderSample]] = self.group_cases() + for case_id in cases_map: + case_objs.append(self.expand_case(case_id=case_id, case_samples=cases_map[case_id])) return Orderform( comment=self.order_comment, samples=self.samples, diff --git a/cg/apps/orderform/utils.py b/cg/apps/orderform/utils.py index 47fc6bccda..e1a93c587e 100644 --- a/cg/apps/orderform/utils.py +++ b/cg/apps/orderform/utils.py @@ -1,5 +1,16 @@ +from cg.models.orders.constants import OrderType from cg.models.orders.excel_sample import ExcelSample +ORDER_TYPES_WITH_CASES = [ + OrderType.BALSAMIC, + OrderType.BALSAMIC_QC, + OrderType.BALSAMIC_UMI, + OrderType.MIP_DNA, + OrderType.MIP_RNA, + OrderType.RNAFUSION, + OrderType.TOMTE, +] + def are_all_samples_metagenome(samples: list[ExcelSample]) -> bool: """Check if all samples are metagenome samples""" From 1d4ace3bfe510908a5ccb61b596eff6bf041c565 Mon Sep 17 00:00:00 2001 From: Clinical Genomics Bot Date: Tue, 3 Dec 2024 12:29:15 +0000 Subject: [PATCH 21/25] =?UTF-8?q?Bump=20version:=2064.5.21=20=E2=86=92=206?= =?UTF-8?q?4.5.22=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 8e1a37bf49..beb48655fd 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 64.5.21 +current_version = 64.5.22 commit = True tag = True tag_name = v{new_version} diff --git a/cg/__init__.py b/cg/__init__.py index c5330b791b..e04ac46c42 100644 --- a/cg/__init__.py +++ b/cg/__init__.py @@ -1,2 +1,2 @@ __title__ = "cg" -__version__ = "64.5.21" +__version__ = "64.5.22" diff --git a/pyproject.toml b/pyproject.toml index e659d97411..cc52a8cb7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "cg" -version = "64.5.21" +version = "64.5.22" description = "Clinical Genomics command center" authors = ["Clinical Genomics "] readme = "README.md" From 50e46f521905c4e1783db0b7003ecb1ad97e4a4c Mon Sep 17 00:00:00 2001 From: Sebastian Diaz Date: Tue, 3 Dec 2024 14:53:52 +0100 Subject: [PATCH 22/25] add pacbio service (#3994) * add pacbio service * add order to endpoiint --- cg/server/endpoints/orders.py | 15 +++--- cg/server/ext.py | 4 ++ .../rules/sample/rules.py | 6 +++ .../workflows/microsalt/validation_service.py | 28 +++-------- .../pacbio_long_read/validation_rules.py | 29 +++++++++++ .../pacbio_long_read/validation_service.py | 48 +++++++++++++++++++ 6 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 cg/services/order_validation_service/workflows/pacbio_long_read/validation_rules.py create mode 100644 cg/services/order_validation_service/workflows/pacbio_long_read/validation_service.py diff --git a/cg/server/endpoints/orders.py b/cg/server/endpoints/orders.py index 34f375cea9..fa0eaa877e 100644 --- a/cg/server/endpoints/orders.py +++ b/cg/server/endpoints/orders.py @@ -44,6 +44,7 @@ mutant_validation_service, order_service, order_submitter_registry, + pacbio_long_read_validation_service, rna_fusion_validation_service, ticket_handler, tomte_validation_service, @@ -274,16 +275,18 @@ def validate_order(order_type: OrderType): response = {} if order_type == OrderType.BALSAMIC: response = balsamic_validation_service.validate(raw_order) - if order_type == OrderType.MICROBIAL_FASTQ: + elif order_type == OrderType.MICROBIAL_FASTQ: response = microbial_fastq_validation_service.validate(raw_order) - if order_type == OrderType.MICROSALT: + elif order_type == OrderType.MICROSALT: response = microsalt_validation_service.validate(raw_order) - if order_type == OrderType.MIP_DNA: + elif order_type == OrderType.MIP_DNA: response = mip_dna_validation_service.validate(raw_order) - if order_type == OrderType.SARS_COV_2: + elif order_type == OrderType.PACBIO_LONG_READ: + response = pacbio_long_read_validation_service.validate(raw_order) + elif order_type == OrderType.SARS_COV_2: response = mutant_validation_service.validate(raw_order) - if order_type == OrderType.RNAFUSION: + elif order_type == OrderType.RNAFUSION: response = rna_fusion_validation_service.validate(raw_order) - if order_type == OrderType.TOMTE: + elif order_type == OrderType.TOMTE: response = tomte_validation_service.validate(raw_order) return jsonify(response), HTTPStatus.OK diff --git a/cg/server/ext.py b/cg/server/ext.py index ff2ed1ad84..69fab5501b 100644 --- a/cg/server/ext.py +++ b/cg/server/ext.py @@ -27,6 +27,9 @@ from cg.services.order_validation_service.workflows.mutant.validation_service import ( MutantValidationService, ) +from cg.services.order_validation_service.workflows.pacbio_long_read.validation_service import ( + PacbioLongReadValidationService, +) from cg.services.order_validation_service.workflows.rna_fusion.validation_service import ( RnaFusionValidationService, ) @@ -123,6 +126,7 @@ def init_app(self, app): microsalt_validation_service = MicroSaltValidationService(store=db) mip_dna_validation_service = MipDnaValidationService(store=db) mutant_validation_service = MutantValidationService(store=db) +pacbio_long_read_validation_service = PacbioLongReadValidationService(store=db) rna_fusion_validation_service = RnaFusionValidationService(store=db) tomte_validation_service = TomteValidationService(store=db) diff --git a/cg/services/order_validation_service/rules/sample/rules.py b/cg/services/order_validation_service/rules/sample/rules.py index cb14e6fb45..17f5865e7d 100644 --- a/cg/services/order_validation_service/rules/sample/rules.py +++ b/cg/services/order_validation_service/rules/sample/rules.py @@ -34,6 +34,9 @@ def validate_application_compatibility( store: Store, **kwargs, ) -> list[ApplicationNotCompatibleError]: + """ + Validate that the applications of all samples in the order are compatible with the order type. + """ errors: list[ApplicationNotCompatibleError] = [] order_type: OrderType = order.order_type for sample_index, sample in order.enumerated_samples: @@ -51,6 +54,7 @@ def validate_application_compatibility( def validate_application_exists( order: OrderWithSamples, store: Store, **kwargs ) -> list[ApplicationNotValidError]: + """Validate that the applications of all samples in the order exist in the database.""" errors: list[ApplicationNotValidError] = [] for sample_index, sample in order.enumerated_samples: if not store.get_application_by_tag(sample.application): @@ -95,6 +99,7 @@ def validate_organism_exists( def validate_sample_names_available( order: OrderWithSamples, store: Store, **kwargs ) -> list[SampleNameNotAvailableError]: + """Validate that the sample names do not exists in the database under the same customer.""" errors: list[SampleNameNotAvailableError] = [] customer = store.get_customer_by_internal_id(order.customer) for sample_index, sample in order.enumerated_samples: @@ -117,6 +122,7 @@ def validate_tube_container_name_unique( order: OrderWithSamples, **kwargs, ) -> list[ContainerNameRepeatedError]: + """Validate that the container names are unique for tube samples.""" errors: list[ContainerNameRepeatedError] = [] repeated_container_name_indices: list = get_indices_for_tube_repeated_container_name(order) for sample_index in repeated_container_name_indices: diff --git a/cg/services/order_validation_service/workflows/microsalt/validation_service.py b/cg/services/order_validation_service/workflows/microsalt/validation_service.py index a199b238d0..369dc8018b 100644 --- a/cg/services/order_validation_service/workflows/microsalt/validation_service.py +++ b/cg/services/order_validation_service/workflows/microsalt/validation_service.py @@ -1,30 +1,16 @@ from cg.services.order_validation_service.errors.order_errors import OrderError from cg.services.order_validation_service.errors.sample_errors import SampleError -from cg.services.order_validation_service.errors.validation_errors import ( - ValidationErrors, -) -from cg.services.order_validation_service.model_validator.model_validator import ( - ModelValidator, -) -from cg.services.order_validation_service.order_validation_service import ( - OrderValidationService, -) -from cg.services.order_validation_service.response_mapper import ( - create_order_validation_response, -) +from cg.services.order_validation_service.errors.validation_errors import ValidationErrors +from cg.services.order_validation_service.model_validator.model_validator import ModelValidator +from cg.services.order_validation_service.order_validation_service import OrderValidationService +from cg.services.order_validation_service.response_mapper import create_order_validation_response from cg.services.order_validation_service.utils import ( apply_order_validation, apply_sample_validation, ) -from cg.services.order_validation_service.workflows.microsalt.models.order import ( - MicrosaltOrder, -) -from cg.services.order_validation_service.workflows.microsalt.validation_rules import ( - SAMPLE_RULES, -) -from cg.services.order_validation_service.workflows.order_validation_rules import ( - ORDER_RULES, -) +from cg.services.order_validation_service.workflows.microsalt.models.order import MicrosaltOrder +from cg.services.order_validation_service.workflows.microsalt.validation_rules import SAMPLE_RULES +from cg.services.order_validation_service.workflows.order_validation_rules import ORDER_RULES from cg.store.store import Store diff --git a/cg/services/order_validation_service/workflows/pacbio_long_read/validation_rules.py b/cg/services/order_validation_service/workflows/pacbio_long_read/validation_rules.py new file mode 100644 index 0000000000..45463ee938 --- /dev/null +++ b/cg/services/order_validation_service/workflows/pacbio_long_read/validation_rules.py @@ -0,0 +1,29 @@ +from cg.services.order_validation_service.rules.sample.rules import ( + validate_application_compatibility, + validate_application_exists, + validate_applications_not_archived, + validate_container_name_required, + validate_sample_names_available, + validate_sample_names_unique, + validate_tube_container_name_unique, + validate_volume_interval, + validate_volume_required, + validate_well_position_format, + validate_well_positions_required, + validate_wells_contain_at_most_one_sample, +) + +SAMPLE_RULES: list[callable] = [ + validate_application_compatibility, + validate_application_exists, + validate_applications_not_archived, + validate_container_name_required, + validate_sample_names_available, + validate_sample_names_unique, + validate_tube_container_name_unique, + validate_volume_interval, + validate_volume_required, + validate_wells_contain_at_most_one_sample, + validate_well_position_format, + validate_well_positions_required, +] diff --git a/cg/services/order_validation_service/workflows/pacbio_long_read/validation_service.py b/cg/services/order_validation_service/workflows/pacbio_long_read/validation_service.py new file mode 100644 index 0000000000..f77f599d4a --- /dev/null +++ b/cg/services/order_validation_service/workflows/pacbio_long_read/validation_service.py @@ -0,0 +1,48 @@ +from cg.services.order_validation_service.errors.order_errors import OrderError +from cg.services.order_validation_service.errors.sample_errors import SampleError +from cg.services.order_validation_service.errors.validation_errors import ValidationErrors +from cg.services.order_validation_service.model_validator.model_validator import ModelValidator +from cg.services.order_validation_service.order_validation_service import OrderValidationService +from cg.services.order_validation_service.response_mapper import create_order_validation_response +from cg.services.order_validation_service.utils import ( + apply_order_validation, + apply_sample_validation, +) +from cg.services.order_validation_service.workflows.order_validation_rules import ORDER_RULES +from cg.services.order_validation_service.workflows.pacbio_long_read.models.order import PacbioOrder +from cg.services.order_validation_service.workflows.pacbio_long_read.validation_rules import ( + SAMPLE_RULES, +) +from cg.store.store import Store + + +class PacbioLongReadValidationService(OrderValidationService): + + def __init__(self, store: Store): + self.store = store + + def validate(self, raw_order: dict) -> dict: + errors: ValidationErrors = self._get_errors(raw_order) + return create_order_validation_response(raw_order=raw_order, errors=errors) + + def _get_errors(self, raw_order: dict) -> ValidationErrors: + order, field_errors = ModelValidator.validate(order=raw_order, model=PacbioOrder) + + if not order: + return field_errors + + order_errors: list[OrderError] = apply_order_validation( + rules=ORDER_RULES, + order=order, + store=self.store, + ) + sample_errors: list[SampleError] = apply_sample_validation( + rules=SAMPLE_RULES, + order=order, + store=self.store, + ) + + return ValidationErrors( + order_errors=order_errors, + sample_errors=sample_errors, + ) From d3b713245b741f1c70bf8e3468c052ffdc4352f2 Mon Sep 17 00:00:00 2001 From: EliottBo <112384714+eliottBo@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:01:06 +0100 Subject: [PATCH 23/25] Add validation case name different from sample name (#3995)(patch) --- .../errors/case_sample_errors.py | 5 +++++ .../rules/case_sample/rules.py | 16 ++++++++++++++++ .../workflows/balsamic/validation_rules.py | 2 ++ .../workflows/mip_dna/validation_rules.py | 2 ++ .../workflows/rna_fusion/validation_rules.py | 2 ++ .../workflows/tomte/validation_rules.py | 2 ++ .../test_case_sample_rules.py | 18 ++++++++++++++++++ 7 files changed, 47 insertions(+) diff --git a/cg/services/order_validation_service/errors/case_sample_errors.py b/cg/services/order_validation_service/errors/case_sample_errors.py index 54e18f149d..d023a816ec 100644 --- a/cg/services/order_validation_service/errors/case_sample_errors.py +++ b/cg/services/order_validation_service/errors/case_sample_errors.py @@ -153,3 +153,8 @@ class WellFormatError(CaseSampleError): class ContainerNameRepeatedError(CaseSampleError): field: str = "container_name" message: str = "Tube names must be unique among samples" + + +class CaseNameSampleNameSameError(CaseSampleError): + field: str = "name" + message: str = "Sample name must be different from case name" diff --git a/cg/services/order_validation_service/rules/case_sample/rules.py b/cg/services/order_validation_service/rules/case_sample/rules.py index 10e4a76ff3..615e89b229 100644 --- a/cg/services/order_validation_service/rules/case_sample/rules.py +++ b/cg/services/order_validation_service/rules/case_sample/rules.py @@ -7,6 +7,7 @@ ApplicationArchivedError, ApplicationNotCompatibleError, ApplicationNotValidError, + CaseNameSampleNameSameError, ConcentrationRequiredIfSkipRCError, ContainerNameMissingError, ContainerNameRepeatedError, @@ -390,3 +391,18 @@ def validate_tube_container_name_unique( error = ContainerNameRepeatedError(case_index=case_index, sample_index=sample_index) errors.append(error) return errors + + +def validate_case_names_different_from_sample_names( + order: OrderWithCases, **kwargs +) -> list[CaseNameSampleNameSameError]: + errors: list[CaseNameSampleNameSameError] = [] + + for case_index, case in order.enumerated_new_cases: + for sample_index, sample in case.enumerated_new_samples: + if sample.name == case.name: + error = CaseNameSampleNameSameError( + case_index=case_index, sample_index=sample_index + ) + errors.append(error) + return errors diff --git a/cg/services/order_validation_service/workflows/balsamic/validation_rules.py b/cg/services/order_validation_service/workflows/balsamic/validation_rules.py index 79acab2d7f..183d0d34a5 100644 --- a/cg/services/order_validation_service/workflows/balsamic/validation_rules.py +++ b/cg/services/order_validation_service/workflows/balsamic/validation_rules.py @@ -8,6 +8,7 @@ validate_application_exists, validate_application_not_archived, validate_buffer_skip_rc_condition, + validate_case_names_different_from_sample_names, validate_concentration_interval_if_skip_rc, validate_concentration_required_if_skip_rc, validate_container_name_required, @@ -39,6 +40,7 @@ validate_application_not_archived, validate_buffer_skip_rc_condition, validate_capture_kit_panel_requirement, + validate_case_names_different_from_sample_names, validate_volume_required, validate_concentration_interval_if_skip_rc, validate_concentration_required_if_skip_rc, diff --git a/cg/services/order_validation_service/workflows/mip_dna/validation_rules.py b/cg/services/order_validation_service/workflows/mip_dna/validation_rules.py index c0331ab877..690843c4ef 100644 --- a/cg/services/order_validation_service/workflows/mip_dna/validation_rules.py +++ b/cg/services/order_validation_service/workflows/mip_dna/validation_rules.py @@ -9,6 +9,7 @@ validate_application_exists, validate_application_not_archived, validate_buffer_skip_rc_condition, + validate_case_names_different_from_sample_names, validate_concentration_interval_if_skip_rc, validate_concentration_required_if_skip_rc, validate_container_name_required, @@ -45,6 +46,7 @@ validate_application_not_archived, validate_buffer_skip_rc_condition, validate_volume_required, + validate_case_names_different_from_sample_names, validate_concentration_interval_if_skip_rc, validate_concentration_required_if_skip_rc, validate_container_name_required, diff --git a/cg/services/order_validation_service/workflows/rna_fusion/validation_rules.py b/cg/services/order_validation_service/workflows/rna_fusion/validation_rules.py index ecaa86fad6..1fde355f1a 100644 --- a/cg/services/order_validation_service/workflows/rna_fusion/validation_rules.py +++ b/cg/services/order_validation_service/workflows/rna_fusion/validation_rules.py @@ -8,6 +8,7 @@ validate_application_exists, validate_application_not_archived, validate_buffer_skip_rc_condition, + validate_case_names_different_from_sample_names, validate_concentration_interval_if_skip_rc, validate_concentration_required_if_skip_rc, validate_container_name_required, @@ -35,6 +36,7 @@ validate_application_exists, validate_application_not_archived, validate_buffer_skip_rc_condition, + validate_case_names_different_from_sample_names, validate_concentration_interval_if_skip_rc, validate_concentration_required_if_skip_rc, validate_container_name_required, diff --git a/cg/services/order_validation_service/workflows/tomte/validation_rules.py b/cg/services/order_validation_service/workflows/tomte/validation_rules.py index ad77f09a0a..8c6ca858e0 100644 --- a/cg/services/order_validation_service/workflows/tomte/validation_rules.py +++ b/cg/services/order_validation_service/workflows/tomte/validation_rules.py @@ -9,6 +9,7 @@ validate_application_exists, validate_application_not_archived, validate_buffer_skip_rc_condition, + validate_case_names_different_from_sample_names, validate_concentration_interval_if_skip_rc, validate_concentration_required_if_skip_rc, validate_container_name_required, @@ -45,6 +46,7 @@ validate_application_not_archived, validate_buffer_skip_rc_condition, validate_volume_required, + validate_case_names_different_from_sample_names, validate_concentration_interval_if_skip_rc, validate_concentration_required_if_skip_rc, validate_container_name_required, diff --git a/tests/services/order_validation_service/test_case_sample_rules.py b/tests/services/order_validation_service/test_case_sample_rules.py index c1ad9c7b08..ee5bbe50ec 100644 --- a/tests/services/order_validation_service/test_case_sample_rules.py +++ b/tests/services/order_validation_service/test_case_sample_rules.py @@ -6,6 +6,7 @@ ApplicationArchivedError, ApplicationNotCompatibleError, ApplicationNotValidError, + CaseNameSampleNameSameError, ConcentrationRequiredIfSkipRCError, ContainerNameMissingError, ContainerNameRepeatedError, @@ -29,6 +30,7 @@ validate_application_exists, validate_application_not_archived, validate_buffers_are_allowed, + validate_case_names_different_from_sample_names, validate_concentration_interval_if_skip_rc, validate_concentration_required_if_skip_rc, validate_container_name_required, @@ -396,3 +398,19 @@ def test_validate_sex_subject_id_clash(valid_order: OrderWithCases, sample_store # THEN an error should be given for the clash assert errors assert isinstance(errors[0], SexSubjectIdError) + + +def test_validate_case_names_different_from_sample_names(valid_order: OrderWithCases): + # GIVEN an order with a sample with the same name as the case + valid_order.cases[0].samples[0].name = valid_order.cases[0].name + + # WHEN validating that the case names are different from the sample names + errors: list[CaseNameSampleNameSameError] = validate_case_names_different_from_sample_names( + valid_order + ) + + # THEN an error should be returned + assert errors + + # THEN the error should concern the same case and sample name + assert isinstance(errors[0], CaseNameSampleNameSameError) From 5e7d8c1c1da470d47db1e95b8bd439a0026f3938 Mon Sep 17 00:00:00 2001 From: Amin <82151354+ahdamin@users.noreply.github.com> Date: Thu, 5 Dec 2024 09:05:12 +0100 Subject: [PATCH 24/25] Improve-order-flow: Add Fluffy (#3934) Add Fluffy case/sample classes Add Fluffy validation rules --- .../order_validation_service/constants.py | 19 ++++++ .../errors/sample_errors.py | 1 - .../workflows/fluffy/__init__.py | 0 .../workflows/fluffy/constants.py | 8 +++ .../workflows/fluffy/models/__init__.py | 0 .../workflows/fluffy/models/order.py | 18 ++++++ .../workflows/fluffy/models/sample.py | 13 ++++ .../workflows/fluffy/validation_rules.py | 29 +++++++++ .../workflows/fluffy/validation_service.py | 60 +++++++++++++++++++ 9 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 cg/services/order_validation_service/workflows/fluffy/__init__.py create mode 100644 cg/services/order_validation_service/workflows/fluffy/constants.py create mode 100644 cg/services/order_validation_service/workflows/fluffy/models/__init__.py create mode 100644 cg/services/order_validation_service/workflows/fluffy/models/order.py create mode 100644 cg/services/order_validation_service/workflows/fluffy/models/sample.py create mode 100644 cg/services/order_validation_service/workflows/fluffy/validation_rules.py create mode 100644 cg/services/order_validation_service/workflows/fluffy/validation_service.py diff --git a/cg/services/order_validation_service/constants.py b/cg/services/order_validation_service/constants.py index efc4585dc3..50dc368e93 100644 --- a/cg/services/order_validation_service/constants.py +++ b/cg/services/order_validation_service/constants.py @@ -25,3 +25,22 @@ class ExtractionMethod(Enum): QIAGEN_MAGATTRACT = "Qiagen MagAttract" QIASYMPHONE = "QIAsymphony" OTHER = 'Other (specify in "Comments")' + + +class IndexEnum(Enum): + IDT_DS_B = "IDT DupSeq 10 bp Set B" + IDT_DS_F = "IDT DupSeq 10 bp Set F" + DT_XGEN_UDI = "IDT xGen UDI Adapters" + TRUSEQ_DNA_HT = "TruSeq DNA HT Dual-index (D7-D5)" + NEXTFLEX_UDI_96 = "NEXTflex UDI Barcodes 1-96" + NEXTFLEX_V2_UDI_96 = "NEXTflex v2 UDI Barcodes 1-96" + NEXTERA_XT = "Nextera XT Dual" + TWIST_UDI_A = "TWIST UDI Set A" + TWIST_UDI_B = "TWIST UDI Set B" + TWIST_UDI_C = "TWIST UDI Set C" + TEN_X_TN_A = "10X Genomics Dual Index kit TN Set A" + TEN_X_TT_A = "10X Genomics Dual Index kit TT Set A" + KAPA_UDI_NIPT = "KAPA UDI NIPT" + AVIDA_INDEX_PLATE = "Avida index plate" + AVIDA_INDEX_STRIP = "Avida index strip" + NOINDEX = "NoIndex" diff --git a/cg/services/order_validation_service/errors/sample_errors.py b/cg/services/order_validation_service/errors/sample_errors.py index 27ffecd4ce..39784b4947 100644 --- a/cg/services/order_validation_service/errors/sample_errors.py +++ b/cg/services/order_validation_service/errors/sample_errors.py @@ -3,7 +3,6 @@ MINIMUM_VOLUME, ) from cg.services.order_validation_service.errors.order_errors import OrderError -from cg.models.orders.sample_base import ContainerEnum class SampleError(OrderError): diff --git a/cg/services/order_validation_service/workflows/fluffy/__init__.py b/cg/services/order_validation_service/workflows/fluffy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cg/services/order_validation_service/workflows/fluffy/constants.py b/cg/services/order_validation_service/workflows/fluffy/constants.py new file mode 100644 index 0000000000..3518ce2232 --- /dev/null +++ b/cg/services/order_validation_service/workflows/fluffy/constants.py @@ -0,0 +1,8 @@ +from enum import Enum + +from cg.constants import DataDelivery + + +class FluffyDeliveryType(Enum): + STATINA = DataDelivery.STATINA + NO_DELIVERY = DataDelivery.NO_DELIVERY diff --git a/cg/services/order_validation_service/workflows/fluffy/models/__init__.py b/cg/services/order_validation_service/workflows/fluffy/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cg/services/order_validation_service/workflows/fluffy/models/order.py b/cg/services/order_validation_service/workflows/fluffy/models/order.py new file mode 100644 index 0000000000..a1cfa81ee1 --- /dev/null +++ b/cg/services/order_validation_service/workflows/fluffy/models/order.py @@ -0,0 +1,18 @@ +from cg.services.order_validation_service.models.order_with_samples import ( + OrderWithSamples, +) +from cg.services.order_validation_service.workflows.fluffy.constants import ( + FluffyDeliveryType, +) +from cg.services.order_validation_service.workflows.fluffy.models.sample import ( + FluffySample, +) + + +class FluffyOrder(OrderWithSamples): + delivery_type: FluffyDeliveryType + samples: list[FluffySample] + + @property + def enumerated_samples(self) -> enumerate[FluffySample]: + return enumerate(self.samples) diff --git a/cg/services/order_validation_service/workflows/fluffy/models/sample.py b/cg/services/order_validation_service/workflows/fluffy/models/sample.py new file mode 100644 index 0000000000..873c650386 --- /dev/null +++ b/cg/services/order_validation_service/workflows/fluffy/models/sample.py @@ -0,0 +1,13 @@ +from cg.models.orders.sample_base import ControlEnum, PriorityEnum +from cg.services.order_validation_service.constants import IndexEnum +from cg.services.order_validation_service.models.sample import Sample + + +class FluffySample(Sample): + control: ControlEnum | None = None + priority: PriorityEnum + index: IndexEnum + index_number: str | None + pool: str + pool_concentration: float + concentration_sample: float | None = None diff --git a/cg/services/order_validation_service/workflows/fluffy/validation_rules.py b/cg/services/order_validation_service/workflows/fluffy/validation_rules.py new file mode 100644 index 0000000000..247b01adc7 --- /dev/null +++ b/cg/services/order_validation_service/workflows/fluffy/validation_rules.py @@ -0,0 +1,29 @@ +from cg.services.order_validation_service.rules.sample.rules import ( + validate_application_compatibility, + validate_application_exists, + validate_applications_not_archived, + validate_container_name_required, + validate_required_volume, + validate_sample_names_available, + validate_sample_names_unique, + validate_tube_container_name_unique, + validate_volume_interval, + validate_well_position_format, + validate_well_positions_required, + validate_wells_contain_at_most_one_sample, +) + +SAMPLE_RULES: list[callable] = [ + validate_application_compatibility, + validate_application_exists, + validate_applications_not_archived, + validate_container_name_required, + validate_required_volume, + validate_sample_names_available, + validate_sample_names_unique, + validate_tube_container_name_unique, + validate_volume_interval, + validate_well_position_format, + validate_well_positions_required, + validate_wells_contain_at_most_one_sample, +] diff --git a/cg/services/order_validation_service/workflows/fluffy/validation_service.py b/cg/services/order_validation_service/workflows/fluffy/validation_service.py new file mode 100644 index 0000000000..5010945a26 --- /dev/null +++ b/cg/services/order_validation_service/workflows/fluffy/validation_service.py @@ -0,0 +1,60 @@ +from cg.services.order_validation_service.errors.order_errors import OrderError +from cg.services.order_validation_service.errors.sample_errors import SampleError +from cg.services.order_validation_service.errors.validation_errors import ( + ValidationErrors, +) +from cg.services.order_validation_service.model_validator.model_validator import ( + ModelValidator, +) +from cg.services.order_validation_service.order_validation_service import ( + OrderValidationService, +) +from cg.services.order_validation_service.response_mapper import ( + create_order_validation_response, +) +from cg.services.order_validation_service.utils import ( + apply_order_validation, + apply_sample_validation, +) +from cg.services.order_validation_service.workflows.fluffy.models.order import ( + FluffyOrder, +) +from cg.services.order_validation_service.workflows.fluffy.validation_rules import ( + SAMPLE_RULES, +) +from cg.services.order_validation_service.workflows.order_validation_rules import ( + ORDER_RULES, +) +from cg.store.store import Store + + +class FluffyValidationService(OrderValidationService): + + def __init__(self, store: Store): + self.store = store + + def validate(self, raw_order: dict) -> dict: + errors: ValidationErrors = self._get_errors(raw_order) + return create_order_validation_response(raw_order=raw_order, errors=errors) + + def _get_errors(self, raw_order: dict) -> ValidationErrors: + order, field_errors = ModelValidator.validate(order=raw_order, model=FluffyOrder) + + if not order: + return field_errors + + order_errors: list[OrderError] = apply_order_validation( + rules=ORDER_RULES, + order=order, + store=self.store, + ) + sample_errors: list[SampleError] = apply_sample_validation( + rules=SAMPLE_RULES, + order=order, + store=self.store, + ) + + return ValidationErrors( + order_errors=order_errors, + sample_errors=sample_errors, + ) From 4dbc0584ef812954592d5eb7624ea4fc20fe666d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isak=20Ohlsson=20=C3=85ngnell?= <40887124+islean@users.noreply.github.com> Date: Thu, 5 Dec 2024 16:39:36 +0100 Subject: [PATCH 25/25] (Improve order flow) Add fastq rules (#3986) (patch) ### Added - Rules and service for the fastq order type --- cg/server/endpoints/orders.py | 3 + cg/server/ext.py | 4 + .../errors/sample_errors.py | 25 ++- .../models/aliases.py | 3 + .../rules/case_sample/utils.py | 66 ++------ .../rules/sample/rules.py | 44 ++++- .../rules/sample/utils.py | 61 ++++++- .../order_validation_service/rules/utils.py | 48 +++++- .../workflows/fastq/validation_rules.py | 35 ++++ .../workflows/fastq/validation_service.py | 46 ++++++ .../sample_rules/conftest.py | 54 +++++-- .../sample_rules/test_data_validators.py | 42 +++-- .../test_inter_field_validators.py | 16 +- .../sample_rules/test_sample_rules.py | 151 +++++++++++++----- 14 files changed, 452 insertions(+), 146 deletions(-) create mode 100644 cg/services/order_validation_service/workflows/fastq/validation_rules.py create mode 100644 cg/services/order_validation_service/workflows/fastq/validation_service.py diff --git a/cg/server/endpoints/orders.py b/cg/server/endpoints/orders.py index fa0eaa877e..02ecc89334 100644 --- a/cg/server/endpoints/orders.py +++ b/cg/server/endpoints/orders.py @@ -37,6 +37,7 @@ balsamic_validation_service, db, delivery_message_service, + fastq_validation_service, lims, microbial_fastq_validation_service, microsalt_validation_service, @@ -275,6 +276,8 @@ def validate_order(order_type: OrderType): response = {} if order_type == OrderType.BALSAMIC: response = balsamic_validation_service.validate(raw_order) + elif order_type == OrderType.FASTQ: + response = fastq_validation_service.validate(raw_order) elif order_type == OrderType.MICROBIAL_FASTQ: response = microbial_fastq_validation_service.validate(raw_order) elif order_type == OrderType.MICROSALT: diff --git a/cg/server/ext.py b/cg/server/ext.py index 69fab5501b..9535862d9b 100644 --- a/cg/server/ext.py +++ b/cg/server/ext.py @@ -15,6 +15,9 @@ from cg.services.order_validation_service.workflows.balsamic.validation_service import ( BalsamicValidationService, ) +from cg.services.order_validation_service.workflows.fastq.validation_service import ( + FastqValidationService, +) from cg.services.order_validation_service.workflows.microbial_fastq.validation_service import ( MicrobialFastqValidationService, ) @@ -122,6 +125,7 @@ def init_app(self, app): ) balsamic_validation_service = BalsamicValidationService(store=db) +fastq_validation_service = FastqValidationService(store=db) microbial_fastq_validation_service = MicrobialFastqValidationService(store=db) microsalt_validation_service = MicroSaltValidationService(store=db) mip_dna_validation_service = MipDnaValidationService(store=db) diff --git a/cg/services/order_validation_service/errors/sample_errors.py b/cg/services/order_validation_service/errors/sample_errors.py index 39784b4947..60d107fb14 100644 --- a/cg/services/order_validation_service/errors/sample_errors.py +++ b/cg/services/order_validation_service/errors/sample_errors.py @@ -1,7 +1,4 @@ -from cg.services.order_validation_service.constants import ( - MAXIMUM_VOLUME, - MINIMUM_VOLUME, -) +from cg.services.order_validation_service.constants import MAXIMUM_VOLUME, MINIMUM_VOLUME from cg.services.order_validation_service.errors.order_errors import OrderError @@ -72,3 +69,23 @@ class WellFormatError(SampleError): class ContainerNameMissingError(SampleError): field: str = "container_name" message: str = "Container must have a name" + + +class BufferInvalidError(SampleError): + field: str = "elution_buffer" + message: str = "Buffer must be Tris-HCl or Nuclease-free water when skipping reception control." + + +class ConcentrationRequiredError(SampleError): + field: str = "concentration_ng_ul" + message: str = "Concentration is required when skipping reception control." + + +class ConcentrationInvalidIfSkipRCError(SampleError): + def __init__(self, sample_index: int, allowed_interval: tuple[float, float]): + field: str = "concentration_ng_ul" + message: str = ( + f"Concentration must be between {allowed_interval[0]} ng/μL and " + f"{allowed_interval[1]} ng/μL if reception control should be skipped" + ) + super(SampleError, self).__init__(sample_index=sample_index, field=field, message=message) diff --git a/cg/services/order_validation_service/models/aliases.py b/cg/services/order_validation_service/models/aliases.py index d8d84c5b8b..1d35731a4e 100644 --- a/cg/services/order_validation_service/models/aliases.py +++ b/cg/services/order_validation_service/models/aliases.py @@ -13,3 +13,6 @@ NonHumanSample = MutantSample | MicrosaltSample HumanSample = BalsamicSample | FastqSample | MipDnaSample | RnaFusionSample | TomteSample + +CaseWithSkipRC = TomteCase | MipDnaCase +SampleWithSkipRC = TomteSample | MipDnaSample | FastqSample diff --git a/cg/services/order_validation_service/rules/case_sample/utils.py b/cg/services/order_validation_service/rules/case_sample/utils.py index ef0a618833..1d2149cf9c 100644 --- a/cg/services/order_validation_service/rules/case_sample/utils.py +++ b/cg/services/order_validation_service/rules/case_sample/utils.py @@ -1,7 +1,6 @@ import re from collections import Counter -from cg.constants.sample_sources import SourceType from cg.constants.subject import Sex from cg.models.orders.sample_base import ContainerEnum, SexEnum from cg.services.order_validation_service.errors.case_errors import RepeatedCaseNameError @@ -16,17 +15,19 @@ ) from cg.services.order_validation_service.models.aliases import ( CaseContainingRelatives, + CaseWithSkipRC, HumanSample, SampleWithRelatives, ) from cg.services.order_validation_service.models.order_with_cases import OrderWithCases from cg.services.order_validation_service.models.sample import Sample from cg.services.order_validation_service.rules.utils import ( + get_concentration_interval, + has_sample_invalid_concentration, is_in_container, is_sample_on_plate, is_volume_within_allowed_interval, ) -from cg.store.models import Application from cg.store.store import Store @@ -204,30 +205,25 @@ def validate_subject_ids_in_case( def validate_concentration_in_case( - case: CaseContainingRelatives, case_index: int, store: Store + case: CaseWithSkipRC, case_index: int, store: Store ) -> list[InvalidConcentrationIfSkipRCError]: errors: list[InvalidConcentrationIfSkipRCError] = [] for sample_index, sample in case.enumerated_new_samples: - if has_sample_invalid_concentration(sample=sample, store=store): - error: InvalidConcentrationIfSkipRCError = create_invalid_concentration_error( - case_index=case_index, - sample=sample, - sample_index=sample_index, - store=store, - ) - errors.append(error) + if application := store.get_application_by_tag(sample.application): + allowed_interval = get_concentration_interval(sample=sample, application=application) + if has_sample_invalid_concentration(sample=sample, allowed_interval=allowed_interval): + error: InvalidConcentrationIfSkipRCError = create_invalid_concentration_error( + case_index=case_index, + sample_index=sample_index, + allowed_interval=allowed_interval, + ) + errors.append(error) return errors def create_invalid_concentration_error( - case_index: int, sample: SampleWithRelatives, sample_index: int, store: Store + case_index: int, sample_index: int, allowed_interval: tuple[float, float] ) -> InvalidConcentrationIfSkipRCError: - application: Application = store.get_application_by_tag(sample.application) - is_cfdna: bool = is_sample_cfdna(sample) - allowed_interval: tuple[float, float] = get_concentration_interval( - application=application, - is_cfdna=is_cfdna, - ) return InvalidConcentrationIfSkipRCError( case_index=case_index, sample_index=sample_index, @@ -235,40 +231,6 @@ def create_invalid_concentration_error( ) -def has_sample_invalid_concentration(sample: SampleWithRelatives, store: Store) -> bool: - application: Application | None = store.get_application_by_tag(sample.application) - if not application: - return False - concentration: float | None = sample.concentration_ng_ul - is_cfdna: bool = is_sample_cfdna(sample) - allowed_interval: tuple[float, float] = get_concentration_interval( - application=application, is_cfdna=is_cfdna - ) - return not is_sample_concentration_within_interval( - concentration=concentration, interval=allowed_interval - ) - - -def is_sample_cfdna(sample: SampleWithRelatives) -> bool: - source = sample.source - return source == SourceType.CELL_FREE_DNA - - -def get_concentration_interval(application: Application, is_cfdna: bool) -> tuple[float, float]: - if is_cfdna: - return ( - application.sample_concentration_minimum_cfdna, - application.sample_concentration_maximum_cfdna, - ) - return application.sample_concentration_minimum, application.sample_concentration_maximum - - -def is_sample_concentration_within_interval( - concentration: float, interval: tuple[float, float] -) -> bool: - return interval[0] <= concentration <= interval[1] - - def is_invalid_plate_well_format(sample: Sample) -> bool: """Check if a sample has an invalid well format.""" correct_well_position_pattern: str = r"^[A-H]:([1-9]|1[0-2])$" diff --git a/cg/services/order_validation_service/rules/sample/rules.py b/cg/services/order_validation_service/rules/sample/rules.py index 17f5865e7d..46e7443343 100644 --- a/cg/services/order_validation_service/rules/sample/rules.py +++ b/cg/services/order_validation_service/rules/sample/rules.py @@ -1,8 +1,12 @@ from cg.models.orders.constants import OrderType +from cg.services.order_validation_service.constants import ALLOWED_SKIP_RC_BUFFERS from cg.services.order_validation_service.errors.sample_errors import ( ApplicationArchivedError, ApplicationNotCompatibleError, ApplicationNotValidError, + BufferInvalidError, + ConcentrationInvalidIfSkipRCError, + ConcentrationRequiredError, ContainerNameMissingError, ContainerNameRepeatedError, InvalidVolumeError, @@ -12,6 +16,7 @@ SampleNameRepeatedError, VolumeRequiredError, WellFormatError, + WellPositionMissingError, ) from cg.services.order_validation_service.rules.sample.utils import ( PlateSamplesValidator, @@ -19,12 +24,15 @@ get_indices_for_tube_repeated_container_name, is_container_name_missing, is_invalid_well_format, + validate_concentration_interval, + validate_concentration_required, ) from cg.services.order_validation_service.rules.utils import ( is_application_compatible, is_volume_invalid, is_volume_missing, ) +from cg.services.order_validation_service.workflows.fastq.models.order import FastqOrder from cg.services.order_validation_service.workflows.microsalt.models.order import OrderWithSamples from cg.store.store import Store @@ -74,6 +82,31 @@ def validate_applications_not_archived( return errors +def validate_buffer_skip_rc_condition(order: FastqOrder, **kwargs) -> list[BufferInvalidError]: + errors: list[BufferInvalidError] = [] + if order.skip_reception_control: + errors.extend(validate_buffers_are_allowed(order)) + return errors + + +def validate_buffers_are_allowed(order: FastqOrder) -> list[BufferInvalidError]: + errors: list[BufferInvalidError] = [] + for sample_index, sample in order.enumerated_samples: + if sample.elution_buffer not in ALLOWED_SKIP_RC_BUFFERS: + error = BufferInvalidError(sample_index=sample_index) + errors.append(error) + return errors + + +def validate_concentration_interval_if_skip_rc( + order: FastqOrder, store: Store, **kwargs +) -> list[ConcentrationInvalidIfSkipRCError]: + errors: list[ConcentrationInvalidIfSkipRCError] = [] + if order.skip_reception_control: + errors.extend(validate_concentration_interval(order=order, store=store)) + return errors + + def validate_container_name_required( order: OrderWithSamples, **kwargs ) -> list[ContainerNameMissingError]: @@ -85,6 +118,15 @@ def validate_container_name_required( return errors +def validate_concentration_required_if_skip_rc( + order: FastqOrder, **kwargs +) -> list[ConcentrationRequiredError]: + errors: list[ConcentrationRequiredError] = [] + if order.skip_reception_control: + errors.extend(validate_concentration_required(order)) + return errors + + def validate_organism_exists( order: OrderWithSamples, store: Store, **kwargs ) -> list[OrganismDoesNotExistError]: @@ -169,6 +211,6 @@ def validate_well_position_format(order: OrderWithSamples, **kwargs) -> list[Wel def validate_well_positions_required( order: OrderWithSamples, **kwargs, -) -> list[OccupiedWellError]: +) -> list[WellPositionMissingError]: plate_samples = PlateSamplesValidator(order) return plate_samples.get_well_position_missing_errors() diff --git a/cg/services/order_validation_service/rules/sample/utils.py b/cg/services/order_validation_service/rules/sample/utils.py index 037fd18462..327cca490b 100644 --- a/cg/services/order_validation_service/rules/sample/utils.py +++ b/cg/services/order_validation_service/rules/sample/utils.py @@ -3,11 +3,23 @@ from cg.models.orders.sample_base import ContainerEnum from cg.services.order_validation_service.errors.sample_errors import ( + ConcentrationInvalidIfSkipRCError, + ConcentrationRequiredError, OccupiedWellError, WellPositionMissingError, ) from cg.services.order_validation_service.models.order_with_samples import OrderWithSamples from cg.services.order_validation_service.models.sample import Sample +from cg.services.order_validation_service.rules.utils import ( + get_application_concentration_interval, + get_concentration_interval, + has_sample_invalid_concentration, + is_sample_cfdna, +) +from cg.services.order_validation_service.workflows.fastq.models.order import FastqOrder +from cg.services.order_validation_service.workflows.fastq.models.sample import FastqSample +from cg.store.models import Application +from cg.store.store import Store class PlateSamplesValidator: @@ -32,13 +44,13 @@ def _initialize_wells(self, order: OrderWithSamples): def get_occupied_well_errors(self) -> list[OccupiedWellError]: """Get errors for samples assigned to wells that are already occupied.""" - conflicting_samples: list[Sample] = [] + conflicting_samples: list[int] = [] for samples_indices in self.wells.values(): if len(samples_indices) > 1: conflicting_samples.extend(samples_indices[1:]) return get_occupied_well_errors(conflicting_samples) - def get_well_position_missing_errors(self) -> list[OccupiedWellError]: + def get_well_position_missing_errors(self) -> list[WellPositionMissingError]: """Get errors for samples missing well positions.""" samples_missing_wells: list[int] = [] for sample_index, sample in self.plate_samples: @@ -52,7 +64,7 @@ def get_occupied_well_errors(sample_indices: list[int]) -> list[OccupiedWellErro def get_missing_well_errors(sample_indices: list[int]) -> list[WellPositionMissingError]: - return [WellPositionMissingError(sample_index) for sample_index in sample_indices] + return [WellPositionMissingError(sample_index=sample_index) for sample_index in sample_indices] def get_indices_for_repeated_sample_names(order: OrderWithSamples) -> list[int]: @@ -90,3 +102,46 @@ def is_container_name_missing(sample: Sample) -> bool: if sample.is_on_plate and not sample.container_name: return True return False + + +def create_invalid_concentration_error( + sample: FastqSample, sample_index: int, store: Store +) -> ConcentrationInvalidIfSkipRCError: + application: Application = store.get_application_by_tag(sample.application) + is_cfdna: bool = is_sample_cfdna(sample) + allowed_interval: tuple[float, float] = get_application_concentration_interval( + application=application, + is_cfdna=is_cfdna, + ) + return ConcentrationInvalidIfSkipRCError( + sample_index=sample_index, + allowed_interval=allowed_interval, + ) + + +def validate_concentration_interval( + order: FastqOrder, store: Store +) -> list[ConcentrationInvalidIfSkipRCError]: + errors: list[ConcentrationInvalidIfSkipRCError] = [] + for sample_index, sample in order.enumerated_samples: + if application := store.get_application_by_tag(sample.application): + allowed_interval = get_concentration_interval(sample=sample, application=application) + if allowed_interval and has_sample_invalid_concentration( + sample=sample, allowed_interval=allowed_interval + ): + error: ConcentrationInvalidIfSkipRCError = create_invalid_concentration_error( + sample=sample, + sample_index=sample_index, + store=store, + ) + errors.append(error) + return errors + + +def validate_concentration_required(order: FastqOrder) -> list[ConcentrationRequiredError]: + errors: list[ConcentrationRequiredError] = [] + for sample_index, sample in order.enumerated_samples: + if not sample.concentration_ng_ul: + error = ConcentrationRequiredError(sample_index=sample_index) + errors.append(error) + return errors diff --git a/cg/services/order_validation_service/rules/utils.py b/cg/services/order_validation_service/rules/utils.py index 06537e4a06..664c7eec80 100644 --- a/cg/services/order_validation_service/rules/utils.py +++ b/cg/services/order_validation_service/rules/utils.py @@ -1,9 +1,8 @@ +from cg.constants.sample_sources import SourceType from cg.models.orders.constants import OrderType from cg.models.orders.sample_base import ContainerEnum -from cg.services.order_validation_service.constants import ( - MAXIMUM_VOLUME, - MINIMUM_VOLUME, -) +from cg.services.order_validation_service.constants import MAXIMUM_VOLUME, MINIMUM_VOLUME +from cg.services.order_validation_service.models.aliases import SampleWithSkipRC from cg.services.order_validation_service.models.sample import Sample from cg.store.models import Application from cg.store.store import Store @@ -41,3 +40,44 @@ def is_volume_missing(sample: Sample) -> bool: if is_in_container(sample.container) and not sample.volume: return True return False + + +def has_sample_invalid_concentration( + sample: SampleWithSkipRC, allowed_interval: tuple[float, float] +) -> bool: + concentration: float = sample.concentration_ng_ul + return not is_sample_concentration_within_interval( + concentration=concentration, interval=allowed_interval + ) + + +def get_concentration_interval( + sample: SampleWithSkipRC, application: Application +) -> tuple[float, float] | None: + is_cfdna: bool = is_sample_cfdna(sample) + allowed_interval: tuple[float, float] = get_application_concentration_interval( + application=application, is_cfdna=is_cfdna + ) + return allowed_interval + + +def is_sample_cfdna(sample: SampleWithSkipRC) -> bool: + source = sample.source + return source == SourceType.CELL_FREE_DNA + + +def get_application_concentration_interval( + application: Application, is_cfdna: bool +) -> tuple[float, float]: + if is_cfdna: + return ( + application.sample_concentration_minimum_cfdna, + application.sample_concentration_maximum_cfdna, + ) + return application.sample_concentration_minimum, application.sample_concentration_maximum + + +def is_sample_concentration_within_interval( + concentration: float, interval: tuple[float, float] +) -> bool: + return interval[0] <= concentration <= interval[1] diff --git a/cg/services/order_validation_service/workflows/fastq/validation_rules.py b/cg/services/order_validation_service/workflows/fastq/validation_rules.py new file mode 100644 index 0000000000..360368548e --- /dev/null +++ b/cg/services/order_validation_service/workflows/fastq/validation_rules.py @@ -0,0 +1,35 @@ +from cg.services.order_validation_service.rules.sample.rules import ( + validate_application_compatibility, + validate_application_exists, + validate_applications_not_archived, + validate_buffer_skip_rc_condition, + validate_concentration_interval_if_skip_rc, + validate_concentration_required_if_skip_rc, + validate_container_name_required, + validate_sample_names_available, + validate_sample_names_unique, + validate_tube_container_name_unique, + validate_volume_interval, + validate_volume_required, + validate_well_position_format, + validate_well_positions_required, + validate_wells_contain_at_most_one_sample, +) + +SAMPLE_RULES: list[callable] = [ + validate_application_compatibility, + validate_application_exists, + validate_applications_not_archived, + validate_buffer_skip_rc_condition, + validate_concentration_required_if_skip_rc, + validate_concentration_interval_if_skip_rc, + validate_container_name_required, + validate_sample_names_available, + validate_sample_names_unique, + validate_tube_container_name_unique, + validate_volume_interval, + validate_volume_required, + validate_wells_contain_at_most_one_sample, + validate_well_position_format, + validate_well_positions_required, +] diff --git a/cg/services/order_validation_service/workflows/fastq/validation_service.py b/cg/services/order_validation_service/workflows/fastq/validation_service.py new file mode 100644 index 0000000000..69a86fbe07 --- /dev/null +++ b/cg/services/order_validation_service/workflows/fastq/validation_service.py @@ -0,0 +1,46 @@ +from cg.exc import OrderError +from cg.services.order_validation_service.errors.sample_errors import SampleError +from cg.services.order_validation_service.errors.validation_errors import ValidationErrors +from cg.services.order_validation_service.model_validator.model_validator import ModelValidator +from cg.services.order_validation_service.order_validation_service import OrderValidationService +from cg.services.order_validation_service.response_mapper import create_order_validation_response +from cg.services.order_validation_service.utils import ( + apply_order_validation, + apply_sample_validation, +) +from cg.services.order_validation_service.workflows.fastq.models.order import FastqOrder +from cg.services.order_validation_service.workflows.fastq.validation_rules import SAMPLE_RULES +from cg.services.order_validation_service.workflows.order_validation_rules import ORDER_RULES +from cg.store.store import Store + + +class FastqValidationService(OrderValidationService): + + def __init__(self, store: Store): + self.store = store + + def validate(self, raw_order: dict) -> dict: + errors: ValidationErrors = self._get_errors(raw_order) + return create_order_validation_response(raw_order=raw_order, errors=errors) + + def _get_errors(self, raw_order: dict) -> ValidationErrors: + order, field_errors = ModelValidator.validate(order=raw_order, model=FastqOrder) + + if not order: + return field_errors + + order_errors: list[OrderError] = apply_order_validation( + rules=ORDER_RULES, + order=order, + store=self.store, + ) + sample_errors: list[SampleError] = apply_sample_validation( + rules=SAMPLE_RULES, + order=order, + store=self.store, + ) + + return ValidationErrors( + sample_errors=sample_errors, + order_errors=order_errors, + ) diff --git a/tests/services/order_validation_service/sample_rules/conftest.py b/tests/services/order_validation_service/sample_rules/conftest.py index 3e55e772a4..e73f5db6d1 100644 --- a/tests/services/order_validation_service/sample_rules/conftest.py +++ b/tests/services/order_validation_service/sample_rules/conftest.py @@ -1,21 +1,18 @@ import pytest from cg.models.orders.constants import OrderType -from cg.models.orders.sample_base import ContainerEnum, PriorityEnum +from cg.models.orders.sample_base import ContainerEnum, PriorityEnum, SexEnum from cg.services.order_validation_service.constants import ( MINIMUM_VOLUME, ElutionBuffer, ExtractionMethod, ) -from cg.services.order_validation_service.workflows.microsalt.constants import ( - MicrosaltDeliveryType, -) -from cg.services.order_validation_service.workflows.microsalt.models.order import ( - MicrosaltOrder, -) -from cg.services.order_validation_service.workflows.microsalt.models.sample import ( - MicrosaltSample, -) +from cg.services.order_validation_service.workflows.fastq.constants import FastqDeliveryType +from cg.services.order_validation_service.workflows.fastq.models.order import FastqOrder +from cg.services.order_validation_service.workflows.fastq.models.sample import FastqSample +from cg.services.order_validation_service.workflows.microsalt.constants import MicrosaltDeliveryType +from cg.services.order_validation_service.workflows.microsalt.models.order import MicrosaltOrder +from cg.services.order_validation_service.workflows.microsalt.models.sample import MicrosaltSample from cg.store.models import Application from cg.store.store import Store @@ -51,7 +48,7 @@ def create_microsalt_order(samples: list[MicrosaltSample]) -> MicrosaltOrder: @pytest.fixture -def valid_order() -> MicrosaltOrder: +def valid_microsalt_order() -> MicrosaltOrder: sample_1: MicrosaltSample = create_microsalt_sample(1) sample_2: MicrosaltSample = create_microsalt_sample(2) sample_3: MicrosaltSample = create_microsalt_sample(3) @@ -82,3 +79,38 @@ def order_with_samples_in_same_well() -> MicrosaltOrder: sample_1: MicrosaltSample = create_microsalt_sample(1) sample_2: MicrosaltSample = create_microsalt_sample(1) return create_microsalt_order([sample_1, sample_2]) + + +def create_fastq_sample(id: int) -> FastqSample: + return FastqSample( + application="WGSPCFC030", + comment="", + container=ContainerEnum.tube, + container_name="Fastq tube", + name=f"fastq-sample-{id}", + volume=54, + concentration_ng_ul=30, + elution_buffer=ElutionBuffer.WATER, + priority=PriorityEnum.priority, + quantity=54, + require_qc_ok=True, + sex=SexEnum.male, + source="blood", + subject_id=f"fastq-subject-{id}", + ) + + +@pytest.fixture +def valid_fastq_order() -> FastqOrder: + sample_1: FastqSample = create_fastq_sample(1) + sample_2: FastqSample = create_fastq_sample(2) + sample_3: FastqSample = create_fastq_sample(3) + samples = [sample_1, sample_2, sample_3] + return FastqOrder( + customer="cust000", + project_type=OrderType.FASTQ, + user_id=0, + delivery_type=FastqDeliveryType.FASTQ, + samples=samples, + name="FastqOrder", + ) diff --git a/tests/services/order_validation_service/sample_rules/test_data_validators.py b/tests/services/order_validation_service/sample_rules/test_data_validators.py index c7fb7611bf..d21aa44777 100644 --- a/tests/services/order_validation_service/sample_rules/test_data_validators.py +++ b/tests/services/order_validation_service/sample_rules/test_data_validators.py @@ -13,23 +13,19 @@ validate_organism_exists, validate_volume_interval, ) -from cg.services.order_validation_service.workflows.microsalt.models.order import ( - MicrosaltOrder, -) -from cg.services.order_validation_service.workflows.microsalt.models.sample import ( - MicrosaltSample, -) +from cg.services.order_validation_service.workflows.microsalt.models.order import MicrosaltOrder +from cg.services.order_validation_service.workflows.microsalt.models.sample import MicrosaltSample from cg.store.models import Application from cg.store.store import Store -def test_applications_exist_sample_order(valid_order: MicrosaltOrder, base_store: Store): +def test_applications_exist_sample_order(valid_microsalt_order: MicrosaltOrder, base_store: Store): # GIVEN an order with a sample with an application which is not found in the database - valid_order.samples[0].application = "Non-existent app tag" + valid_microsalt_order.samples[0].application = "Non-existent app tag" # WHEN validating that the specified applications exist - errors = validate_application_exists(order=valid_order, store=base_store) + errors = validate_application_exists(order=valid_microsalt_order, store=base_store) # THEN an error should be returned assert errors @@ -39,16 +35,16 @@ def test_applications_exist_sample_order(valid_order: MicrosaltOrder, base_store def test_application_is_incompatible( - valid_order: MicrosaltOrder, + valid_microsalt_order: MicrosaltOrder, sample_with_non_compatible_application: MicrosaltSample, base_store: Store, ): # GIVEN an order that has a sample with an application which is incompatible with microsalt - valid_order.samples.append(sample_with_non_compatible_application) + valid_microsalt_order.samples.append(sample_with_non_compatible_application) # WHEN validating the order - errors = validate_application_compatibility(order=valid_order, store=base_store) + errors = validate_application_compatibility(order=valid_microsalt_order, store=base_store) # THEN an error should be returned assert errors @@ -58,16 +54,16 @@ def test_application_is_incompatible( def test_application_is_not_archived( - valid_order: MicrosaltOrder, archived_application: Application, base_store: Store + valid_microsalt_order: MicrosaltOrder, archived_application: Application, base_store: Store ): # GIVEN an order with a new sample with an archived application - valid_order.samples[0].application = archived_application.tag + valid_microsalt_order.samples[0].application = archived_application.tag base_store.session.add(archived_application) base_store.commit_to_store() # WHEN validating that the applications are not archived - errors = validate_applications_not_archived(order=valid_order, store=base_store) + errors = validate_applications_not_archived(order=valid_microsalt_order, store=base_store) # THEN an error should be returned assert errors @@ -76,13 +72,13 @@ def test_application_is_not_archived( assert isinstance(errors[0], ApplicationArchivedError) -def test_invalid_volume(valid_order: MicrosaltOrder, base_store: Store): +def test_invalid_volume(valid_microsalt_order: MicrosaltOrder, base_store: Store): # GIVEN an order with a sample with an invalid volume - valid_order.samples[0].volume = MAXIMUM_VOLUME + 10 + valid_microsalt_order.samples[0].volume = MAXIMUM_VOLUME + 10 # WHEN validating the volume interval - errors = validate_volume_interval(order=valid_order) + errors = validate_volume_interval(order=valid_microsalt_order) # THEN an error should be returned assert errors @@ -91,13 +87,13 @@ def test_invalid_volume(valid_order: MicrosaltOrder, base_store: Store): assert isinstance(errors[0], InvalidVolumeError) -def test_invalid_organism(valid_order: MicrosaltOrder, base_store: Store): +def test_invalid_organism(valid_microsalt_order: MicrosaltOrder, base_store: Store): # GIVEN an order with a sample specifying a non-existent organism - valid_order.samples[0].organism = "Non-existent organism" + valid_microsalt_order.samples[0].organism = "Non-existent organism" # WHEN validating that all organisms exist - errors = validate_organism_exists(order=valid_order, store=base_store) + errors = validate_organism_exists(order=valid_microsalt_order, store=base_store) # THEN an error should be returned assert errors @@ -106,12 +102,12 @@ def test_invalid_organism(valid_order: MicrosaltOrder, base_store: Store): assert isinstance(errors[0], OrganismDoesNotExistError) -def test_valid_organisms(valid_order: MicrosaltOrder, base_store: Store): +def test_valid_organisms(valid_microsalt_order: MicrosaltOrder, base_store: Store): # GIVEN a valid order # WHEN validating that all organisms exist - errors = validate_organism_exists(order=valid_order, store=base_store) + errors = validate_organism_exists(order=valid_microsalt_order, store=base_store) # THEN no error should be returned assert not errors diff --git a/tests/services/order_validation_service/sample_rules/test_inter_field_validators.py b/tests/services/order_validation_service/sample_rules/test_inter_field_validators.py index 3e6b475048..6ec4f17e9b 100644 --- a/tests/services/order_validation_service/sample_rules/test_inter_field_validators.py +++ b/tests/services/order_validation_service/sample_rules/test_inter_field_validators.py @@ -6,9 +6,7 @@ validate_sample_names_unique, validate_wells_contain_at_most_one_sample, ) -from cg.services.order_validation_service.workflows.microsalt.models.order import ( - MicrosaltOrder, -) +from cg.services.order_validation_service.workflows.microsalt.models.order import MicrosaltOrder def test_multiple_samples_in_well_not_allowed(order_with_samples_in_same_well: MicrosaltOrder): @@ -25,25 +23,25 @@ def test_multiple_samples_in_well_not_allowed(order_with_samples_in_same_well: M assert isinstance(errors[0], OccupiedWellError) -def test_order_without_multiple_samples_in_well(valid_order: MicrosaltOrder): +def test_order_without_multiple_samples_in_well(valid_microsalt_order: MicrosaltOrder): # GIVEN a valid order with no samples in the same well # WHEN validating the order - errors = validate_wells_contain_at_most_one_sample(valid_order) + errors = validate_wells_contain_at_most_one_sample(valid_microsalt_order) # THEN no errors should be returned assert not errors -def test_sample_name_repeated(valid_order: MicrosaltOrder): +def test_sample_name_repeated(valid_microsalt_order: MicrosaltOrder): # GIVEN a valid order within sample names are repeated - sample_name_1 = valid_order.samples[0].name - valid_order.samples[1].name = sample_name_1 + sample_name_1 = valid_microsalt_order.samples[0].name + valid_microsalt_order.samples[1].name = sample_name_1 # WHEN validating that the sample names are unique - errors = validate_sample_names_unique(valid_order) + errors = validate_sample_names_unique(valid_microsalt_order) # THEN an error should be returned assert errors diff --git a/tests/services/order_validation_service/sample_rules/test_sample_rules.py b/tests/services/order_validation_service/sample_rules/test_sample_rules.py index aa3385e051..777765cd96 100644 --- a/tests/services/order_validation_service/sample_rules/test_sample_rules.py +++ b/tests/services/order_validation_service/sample_rules/test_sample_rules.py @@ -1,5 +1,9 @@ from cg.models.orders.sample_base import ContainerEnum +from cg.services.order_validation_service.constants import ElutionBuffer from cg.services.order_validation_service.errors.sample_errors import ( + BufferInvalidError, + ConcentrationInvalidIfSkipRCError, + ConcentrationRequiredError, ContainerNameMissingError, ContainerNameRepeatedError, SampleNameNotAvailableError, @@ -7,26 +11,30 @@ WellFormatError, ) from cg.services.order_validation_service.rules.sample.rules import ( + validate_buffer_skip_rc_condition, + validate_concentration_interval_if_skip_rc, + validate_concentration_required_if_skip_rc, validate_container_name_required, validate_sample_names_available, validate_tube_container_name_unique, validate_volume_required, validate_well_position_format, ) +from cg.services.order_validation_service.workflows.fastq.models.order import FastqOrder from cg.services.order_validation_service.workflows.microsalt.models.order import MicrosaltOrder from cg.store.models import Sample from cg.store.store import Store -def test_sample_names_available(valid_order: MicrosaltOrder, sample_store: Store): +def test_sample_names_available(valid_microsalt_order: MicrosaltOrder, sample_store: Store): # GIVEN an order with a sample name reused from a previous order sample = sample_store.session.query(Sample).first() - valid_order.customer = sample.customer.internal_id - valid_order.samples[0].name = sample.name + valid_microsalt_order.customer = sample.customer.internal_id + valid_microsalt_order.samples[0].name = sample.name # WHEN validating that the sample names are available to the customer - errors = validate_sample_names_available(order=valid_order, store=sample_store) + errors = validate_sample_names_available(order=valid_microsalt_order, store=sample_store) # THEN an error should be returned assert errors @@ -35,18 +43,18 @@ def test_sample_names_available(valid_order: MicrosaltOrder, sample_store: Store assert isinstance(errors[0], SampleNameNotAvailableError) -def test_validate_tube_container_name_unique(valid_order: MicrosaltOrder): +def test_validate_tube_container_name_unique(valid_microsalt_order: MicrosaltOrder): # GIVEN an order with three samples in tubes with 2 reused container names - valid_order.samples[0].container = ContainerEnum.tube - valid_order.samples[1].container = ContainerEnum.tube - valid_order.samples[2].container = ContainerEnum.tube - valid_order.samples[0].container_name = "container_name" - valid_order.samples[1].container_name = "container_name" - valid_order.samples[2].container_name = "ContainerName" + valid_microsalt_order.samples[0].container = ContainerEnum.tube + valid_microsalt_order.samples[1].container = ContainerEnum.tube + valid_microsalt_order.samples[2].container = ContainerEnum.tube + valid_microsalt_order.samples[0].container_name = "container_name" + valid_microsalt_order.samples[1].container_name = "container_name" + valid_microsalt_order.samples[2].container_name = "ContainerName" # WHEN validating the container names are unique - errors = validate_tube_container_name_unique(order=valid_order) + errors = validate_tube_container_name_unique(order=valid_microsalt_order) # THEN the error should concern the reused container name assert isinstance(errors[0], ContainerNameRepeatedError) @@ -54,13 +62,13 @@ def test_validate_tube_container_name_unique(valid_order: MicrosaltOrder): assert errors[1].sample_index == 1 -def test_validate_well_position_format(valid_order: MicrosaltOrder): +def test_validate_well_position_format(valid_microsalt_order: MicrosaltOrder): # GIVEN an order with a sample with an invalid well position - valid_order.samples[0].well_position = "J:4" + valid_microsalt_order.samples[0].well_position = "J:4" # WHEN validating the well position format - errors = validate_well_position_format(order=valid_order) + errors = validate_well_position_format(order=valid_microsalt_order) # THEN an error should be returned assert errors @@ -70,14 +78,14 @@ def test_validate_well_position_format(valid_order: MicrosaltOrder): assert errors[0].sample_index == 0 -def test_validate_missing_container_name(valid_order: MicrosaltOrder): +def test_validate_missing_container_name(valid_microsalt_order: MicrosaltOrder): # GIVEN an order with a sample on a plate with no container name - valid_order.samples[0].container = ContainerEnum.plate - valid_order.samples[0].container_name = None + valid_microsalt_order.samples[0].container = ContainerEnum.plate + valid_microsalt_order.samples[0].container_name = None # WHEN validating the container name - errors = validate_container_name_required(order=valid_order) + errors = validate_container_name_required(order=valid_microsalt_order) # THEN am error should be returned assert errors @@ -87,46 +95,46 @@ def test_validate_missing_container_name(valid_order: MicrosaltOrder): assert errors[0].sample_index == 0 -def test_validate_valid_container_name(valid_order: MicrosaltOrder): +def test_validate_valid_container_name(valid_microsalt_order: MicrosaltOrder): # GIVEN an order with a sample on a plate with a valid container name - valid_order.samples[0].container = ContainerEnum.plate - valid_order.samples[0].container_name = "Plate_123" + valid_microsalt_order.samples[0].container = ContainerEnum.plate + valid_microsalt_order.samples[0].container_name = "Plate_123" # WHEN validating the container name - errors = validate_container_name_required(order=valid_order) + errors = validate_container_name_required(order=valid_microsalt_order) # THEN no error should be returned assert not errors -def test_validate_non_plate_container(valid_order: MicrosaltOrder): +def test_validate_non_plate_container(valid_microsalt_order: MicrosaltOrder): # GIVEN an order with missing container names but the samples are not on plates - valid_order.samples[0].container = ContainerEnum.tube - valid_order.samples[0].container_name = None + valid_microsalt_order.samples[0].container = ContainerEnum.tube + valid_microsalt_order.samples[0].container_name = None - valid_order.samples[1].container = ContainerEnum.no_container - valid_order.samples[1].container_name = None + valid_microsalt_order.samples[1].container = ContainerEnum.no_container + valid_microsalt_order.samples[1].container_name = None # WHEN validating the container name - errors = validate_container_name_required(order=valid_order) + errors = validate_container_name_required(order=valid_microsalt_order) # THEN no error should be returned assert not errors -def test_missing_required_sample_volume(valid_order: MicrosaltOrder): +def test_missing_required_sample_volume(valid_microsalt_order: MicrosaltOrder): # GIVEN an order with containerized samples missing volume - valid_order.samples[0].container = ContainerEnum.tube - valid_order.samples[0].volume = None + valid_microsalt_order.samples[0].container = ContainerEnum.tube + valid_microsalt_order.samples[0].volume = None - valid_order.samples[1].container = ContainerEnum.plate - valid_order.samples[1].volume = None + valid_microsalt_order.samples[1].container = ContainerEnum.plate + valid_microsalt_order.samples[1].volume = None # WHEN validating the volume - errors = validate_volume_required(order=valid_order) + errors = validate_volume_required(order=valid_microsalt_order) # THEN an error should be returned assert errors @@ -139,14 +147,79 @@ def test_missing_required_sample_volume(valid_order: MicrosaltOrder): assert errors[1].sample_index == 1 -def test_non_required_sample_volume(valid_order: MicrosaltOrder): +def test_non_required_sample_volume(valid_microsalt_order: MicrosaltOrder): # GIVEN an order with a sample not in a container and no volume set - valid_order.samples[0].container = ContainerEnum.no_container - valid_order.samples[0].volume = None + valid_microsalt_order.samples[0].container = ContainerEnum.no_container + valid_microsalt_order.samples[0].volume = None # WHEN validating the volume - errors = validate_volume_required(order=valid_order) + errors = validate_volume_required(order=valid_microsalt_order) # THEN no error should be returned assert not errors + + +def test_validate_concentration_required_if_skip_rc(valid_fastq_order: FastqOrder): + + # GIVEN a fastq order trying to skip reception control + valid_fastq_order.skip_reception_control = True + + # GIVEN that one of its samples has no concentration set + valid_fastq_order.samples[0].concentration_ng_ul = None + + # WHEN validating that the concentration is not missing + errors: list[ConcentrationRequiredError] = validate_concentration_required_if_skip_rc( + order=valid_fastq_order + ) + + # THEN an error should be returned + assert errors + + # THEN the error should concern the missing concentration + assert isinstance(errors[0], ConcentrationRequiredError) + + +def test_validate_concentration_interval_if_skip_rc( + valid_fastq_order: FastqOrder, base_store: Store +): + + # GIVEN a Fastq order trying to skip reception control + valid_fastq_order.skip_reception_control = True + + # GIVEN that one of the samples has a concentration outside the allowed interval for its + # application + sample = valid_fastq_order.samples[0] + application = base_store.get_application_by_tag(sample.application) + application.sample_concentration_minimum = sample.concentration_ng_ul + 1 + base_store.session.add(application) + base_store.commit_to_store() + + # WHEN validating that the order's samples' concentrations are within allowed intervals + errors: list[ConcentrationInvalidIfSkipRCError] = validate_concentration_interval_if_skip_rc( + order=valid_fastq_order, store=base_store + ) + + # THEN an error should be returned + assert errors + + # THEN the error should concern the concentration level + assert isinstance(errors[0], ConcentrationInvalidIfSkipRCError) + + +def test_validate_buffer_skip_rc_condition(valid_fastq_order: FastqOrder): + + # GIVEN a Fastq order trying to skip reception control + valid_fastq_order.skip_reception_control = True + + # GIVEN that one of the samples has buffer specified as 'other' + valid_fastq_order.samples[0].elution_buffer = ElutionBuffer.OTHER + + # WHEN validating that the buffers follow the 'skip reception control' requirements + errors: list[BufferInvalidError] = validate_buffer_skip_rc_condition(order=valid_fastq_order) + + # THEN an error should be returned + assert errors + + # THEN the error should concern the buffer + assert isinstance(errors[0], BufferInvalidError)