diff --git a/cg/apps/coverage/api.py b/cg/apps/coverage/api.py index 1e15782908..af4d7779bc 100644 --- a/cg/apps/coverage/api.py +++ b/cg/apps/coverage/api.py @@ -1,7 +1,6 @@ """Chanjo API""" import logging import tempfile -from typing import Optional from cg.constants.constants import FileFormat from cg.io.controller import ReadStream @@ -41,7 +40,7 @@ def upload( self.process.run_command(parameters=load_parameters) - def sample(self, sample_id: str) -> Optional[dict]: + def sample(self, sample_id: str) -> dict | None: """Fetch sample from the database""" sample_parameters = ["db", "samples", "-s", sample_id] diff --git a/cg/apps/crunchy/crunchy.py b/cg/apps/crunchy/crunchy.py index 1d56155a63..406515e0be 100644 --- a/cg/apps/crunchy/crunchy.py +++ b/cg/apps/crunchy/crunchy.py @@ -8,7 +8,6 @@ import datetime import logging from pathlib import Path -from typing import Optional from cg.apps.crunchy import files from cg.apps.crunchy.models import CrunchyFile, CrunchyMetadata @@ -33,7 +32,7 @@ class CrunchyAPI: """ def __init__(self, config: dict): - self.conda_binary: Optional[str] = config["crunchy"]["conda_binary"] or None + self.conda_binary: str | None = config["crunchy"]["conda_binary"] or None self.crunchy_env: str = config["crunchy"]["slurm"]["conda_env"] self.dry_run: bool = False self.reference_path: str = config["crunchy"]["cram_reference"] @@ -153,7 +152,7 @@ def is_fastq_compression_done(compression: CompressionData) -> bool: ) # Check if the SPRING archive has been unarchived - updated_at: Optional[datetime.date] = files.get_file_updated_at(crunchy_metadata) + updated_at: datetime.date | None = files.get_file_updated_at(crunchy_metadata) if updated_at is None: LOG.info(f"FASTQ compression is done for {compression.run_name}") return True diff --git a/cg/apps/crunchy/files.py b/cg/apps/crunchy/files.py index 6b7b436266..cddcdc6c8b 100644 --- a/cg/apps/crunchy/files.py +++ b/cg/apps/crunchy/files.py @@ -1,9 +1,8 @@ import logging import tempfile -from datetime import datetime +from datetime import date, datetime from json.decoder import JSONDecodeError from pathlib import Path -from typing import Optional from cg.apps.crunchy.models import CrunchyFile, CrunchyMetadata from cg.constants.constants import FileFormat @@ -64,7 +63,7 @@ def get_crunchy_metadata(metadata_path: Path) -> CrunchyMetadata: return metadata -def get_file_updated_at(crunchy_metadata: CrunchyMetadata) -> Optional[datetime.date]: +def get_file_updated_at(crunchy_metadata: CrunchyMetadata) -> date | None: """Check if a SPRING metadata file has been updated and return the date when updated""" return crunchy_metadata.files[0].updated diff --git a/cg/apps/crunchy/models.py b/cg/apps/crunchy/models.py index 3f0622d25b..4f44301823 100644 --- a/cg/apps/crunchy/models.py +++ b/cg/apps/crunchy/models.py @@ -1,5 +1,4 @@ from datetime import date -from typing import Optional from pydantic import BaseModel, conlist from typing_extensions import Literal @@ -8,9 +7,9 @@ class CrunchyFile(BaseModel): path: str file: Literal["first_read", "second_read", "spring"] - checksum: Optional[str] = None - algorithm: Optional[Literal["sha1", "md5", "sha256"]] = None - updated: Optional[date] = None + checksum: str | None = None + algorithm: Literal["sha1", "md5", "sha256"] | None = None + updated: date | None = None class CrunchyMetadata(BaseModel): diff --git a/cg/apps/demultiplex/demultiplex_api.py b/cg/apps/demultiplex/demultiplex_api.py index bf6ccfa7e3..4a168e33ad 100644 --- a/cg/apps/demultiplex/demultiplex_api.py +++ b/cg/apps/demultiplex/demultiplex_api.py @@ -1,7 +1,6 @@ """This api should handle everything around demultiplexing.""" import logging from pathlib import Path -from typing import Optional from typing_extensions import Literal @@ -28,9 +27,7 @@ class DemultiplexingAPI: This includes starting demultiplexing, creating sample sheets, creating base masks, """ - def __init__( - self, config: dict, housekeeper_api: HousekeeperAPI, out_dir: Optional[Path] = None - ): + def __init__(self, config: dict, housekeeper_api: HousekeeperAPI, out_dir: Path | None = None): self.slurm_api = SlurmAPI() self.hk_api = housekeeper_api self.slurm_account: str = config["demultiplex"]["slurm"]["account"] diff --git a/cg/apps/demultiplex/sample_sheet/index.py b/cg/apps/demultiplex/sample_sheet/index.py index 08be84d666..68d64b625c 100644 --- a/cg/apps/demultiplex/sample_sheet/index.py +++ b/cg/apps/demultiplex/sample_sheet/index.py @@ -1,6 +1,5 @@ """Functions that deal with modifications of the indexes.""" import logging -from typing import Union from packaging import version from pydantic import BaseModel @@ -219,7 +218,7 @@ def pad_and_reverse_complement_sample_indexes( def update_indexes_for_samples( - samples: list[Union[FlowCellSampleBCLConvert, FlowCellSampleBcl2Fastq]], + samples: list[FlowCellSampleBCLConvert | FlowCellSampleBcl2Fastq], index_cycles: int, is_reverse_complement: bool, ) -> None: diff --git a/cg/apps/demultiplex/sample_sheet/sample_sheet_creator.py b/cg/apps/demultiplex/sample_sheet/sample_sheet_creator.py index bcebe28326..1744b3c1a6 100644 --- a/cg/apps/demultiplex/sample_sheet/sample_sheet_creator.py +++ b/cg/apps/demultiplex/sample_sheet/sample_sheet_creator.py @@ -1,6 +1,6 @@ """ Create a sample sheet for NovaSeq flow cells.""" import logging -from typing import Optional, Type, Union +from typing import Type from cg.apps.demultiplex.sample_sheet.index import ( Index, @@ -37,17 +37,15 @@ class SampleSheetCreator: def __init__( self, flow_cell: FlowCellDirectoryData, - lims_samples: list[Union[FlowCellSampleBCLConvert, FlowCellSampleBcl2Fastq]], + lims_samples: list[FlowCellSampleBCLConvert | FlowCellSampleBcl2Fastq], force: bool = False, ): self.flow_cell: FlowCellDirectoryData = flow_cell self.flow_cell_id: str = flow_cell.id - self.lims_samples: list[ - Union[FlowCellSampleBCLConvert, FlowCellSampleBcl2Fastq] - ] = lims_samples + self.lims_samples: list[FlowCellSampleBCLConvert | FlowCellSampleBcl2Fastq] = lims_samples self.run_parameters: RunParameters = flow_cell.run_parameters self.sample_type: Type[ - Union[FlowCellSampleBCLConvert, FlowCellSampleBcl2Fastq] + FlowCellSampleBCLConvert | FlowCellSampleBcl2Fastq ] = flow_cell.sample_type self.force: bool = force @@ -79,7 +77,7 @@ def remove_unwanted_samples(self) -> None: """Filter out samples with single indexes.""" LOG.info("Removing all samples without dual indexes") samples_to_keep = [] - sample: Union[FlowCellSampleBCLConvert, FlowCellSampleBcl2Fastq] + sample: FlowCellSampleBCLConvert | FlowCellSampleBcl2Fastq for sample in self.lims_samples: if not is_dual_index(sample.index): LOG.warning(f"Removing sample {sample} since it does not have dual index") @@ -89,7 +87,7 @@ def remove_unwanted_samples(self) -> None: @staticmethod def convert_sample_to_header_dict( - sample: Union[FlowCellSampleBCLConvert, FlowCellSampleBcl2Fastq], + sample: FlowCellSampleBCLConvert | FlowCellSampleBcl2Fastq, data_column_names: list[str], ) -> list[str]: """Convert a lims sample object to a list that corresponds to the sample sheet headers.""" @@ -97,11 +95,11 @@ def convert_sample_to_header_dict( sample_dict = sample.model_dump(by_alias=True) return [str(sample_dict[column]) for column in data_column_names] - def get_additional_sections_sample_sheet(self) -> Optional[list]: + def get_additional_sections_sample_sheet(self) -> list | None: """Return all sections of the sample sheet that are not the data section.""" raise NotImplementedError("Impossible to get sample sheet sections from parent class") - def get_data_section_header_and_columns(self) -> Optional[list[list[str]]]: + def get_data_section_header_and_columns(self) -> list[list[str]] | None: """Return the header and column names of the data section of the sample sheet.""" raise NotImplementedError("Impossible to get sample sheet sections from parent class") @@ -123,7 +121,7 @@ def create_sample_sheet_content(self) -> list[list[str]]: def process_samples_for_sample_sheet(self) -> None: """Remove unwanted samples and adapt remaining samples.""" self.remove_unwanted_samples() - samples_in_lane: list[Union[FlowCellSampleBCLConvert, FlowCellSampleBcl2Fastq]] + samples_in_lane: list[FlowCellSampleBCLConvert | FlowCellSampleBcl2Fastq] self.add_override_cycles_to_samples() for lane, samples_in_lane in get_samples_by_lane(self.lims_samples).items(): LOG.info(f"Adapting index and barcode mismatch values for samples in lane {lane}") diff --git a/cg/apps/hermes/hermes_api.py b/cg/apps/hermes/hermes_api.py index 0d5a6bb00c..395ebc57db 100644 --- a/cg/apps/hermes/hermes_api.py +++ b/cg/apps/hermes/hermes_api.py @@ -1,7 +1,6 @@ import logging from datetime import datetime from pathlib import Path -from typing import Optional from cg.apps.housekeeper import models as hk_models from cg.utils.commands import Process @@ -18,7 +17,7 @@ def __init__(self, config: dict): self.process = Process(binary=config["hermes"]["binary_path"]) def convert_deliverables( - self, deliverables_file: Path, pipeline: str, analysis_type: Optional[str] = None + self, deliverables_file: Path, pipeline: str, analysis_type: str | None = None ) -> CGDeliverables: """Convert deliverables file in raw pipeline format to CG format with hermes""" LOG.info("Converting pipeline deliverables to CG deliverables") @@ -40,8 +39,8 @@ def create_housekeeper_bundle( bundle_name: str, deliverables: Path, pipeline: str, - analysis_type: Optional[str], - created: Optional[datetime], + analysis_type: str | None, + created: datetime | None, ) -> hk_models.InputBundle: """Convert pipeline deliverables to housekeeper bundle ready to be inserted into hk""" cg_deliverables: CGDeliverables = self.convert_deliverables( @@ -53,7 +52,7 @@ def create_housekeeper_bundle( @staticmethod def get_housekeeper_bundle( - deliverables: CGDeliverables, bundle_name: str, created: Optional[datetime] = None + deliverables: CGDeliverables, bundle_name: str, created: datetime | None = None ) -> hk_models.InputBundle: """Convert a deliverables object to a housekeeper object""" bundle_info = { diff --git a/cg/apps/hermes/models.py b/cg/apps/hermes/models.py index a2735e2eb5..db015ff89f 100644 --- a/cg/apps/hermes/models.py +++ b/cg/apps/hermes/models.py @@ -1,7 +1,6 @@ """Models used by hermes <-> cg interactions""" import logging from pathlib import Path -from typing import Optional from pydantic import BaseModel, field_validator @@ -15,7 +14,7 @@ class CGTag(BaseModel): path: str tags: list[str] - mandatory: Optional[bool] = False + mandatory: bool | None = False class CGDeliverables(BaseModel): diff --git a/cg/apps/housekeeper/hk.py b/cg/apps/housekeeper/hk.py index ffc085aa8d..211d342499 100644 --- a/cg/apps/housekeeper/hk.py +++ b/cg/apps/housekeeper/hk.py @@ -3,7 +3,6 @@ import logging import os from pathlib import Path -from typing import Optional from housekeeper.include import checksum as hk_checksum from housekeeper.include import include_version @@ -77,7 +76,7 @@ def new_file( tags = [] return self._store.new_file(path, checksum, to_archive, tags) - def get_file(self, file_id: int) -> Optional[File]: + def get_file(self, file_id: int) -> File | None: """Get a file based on file id.""" LOG.info(f"Return file: {file_id}") file_obj: File = self._store.get_file_by_id(file_id=file_id) @@ -86,7 +85,7 @@ def get_file(self, file_id: int) -> Optional[File]: return None return file_obj - def delete_file(self, file_id: int) -> Optional[File]: + def delete_file(self, file_id: int) -> File | None: """Delete a file both from database and disk (if included).""" file_obj: File = self.get_file(file_id) if not file_obj: @@ -136,7 +135,7 @@ def files( bundle_name=bundle, tag_names=tags, version_id=version, file_path=path ) - def get_file_insensitive_path(self, path: Path) -> Optional[File]: + def get_file_insensitive_path(self, path: Path) -> File | None: """Returns a file in Housekeeper with a path that matches the given path, insensitive to whether the paths are included or not.""" file: File = self.files(path=path.as_posix()) @@ -148,7 +147,7 @@ def get_file_insensitive_path(self, path: Path) -> Optional[File]: return file @staticmethod - def get_files_from_version(version: Version, tags: set[str]) -> Optional[list[File]]: + def get_files_from_version(version: Version, tags: set[str]) -> list[File] | None: """Return a list of files associated with the given version and tags.""" LOG.debug(f"Getting files from version with tags {tags}") files: list[File] = [] @@ -162,13 +161,13 @@ def get_files_from_version(version: Version, tags: set[str]) -> Optional[list[Fi return files @staticmethod - def get_file_from_version(version: Version, tags: set[str]) -> Optional[File]: + def get_file_from_version(version: Version, tags: set[str]) -> File | None: """Return the first file matching the given tags.""" files: list[File] = HousekeeperAPI.get_files_from_version(version=version, tags=tags) return files[0] if files else None @staticmethod - def get_latest_file_from_version(version: Version, tags: set[str]) -> Optional[File]: + def get_latest_file_from_version(version: Version, tags: set[str]) -> File | None: """Return the latest file from Housekeeper given its version and tags.""" files: list[File] = HousekeeperAPI.get_files_from_version(version=version, tags=tags) return sorted(files, key=lambda file_obj: file_obj.id)[-1] if files else None @@ -181,17 +180,15 @@ def session_no_autoflush(self): """Wrap property in Housekeeper Store.""" return self._store.session.no_autoflush - def get_files( - self, bundle: str, tags: Optional[list] = None, version: Optional[int] = None - ) -> Query: + def get_files(self, bundle: str, tags: list | None = None, version: int | None = None) -> Query: """Get all the files in housekeeper, optionally filtered by bundle and/or tags and/or version. """ return self._store.get_files(bundle_name=bundle, tag_names=tags, version_id=version) def get_latest_file( - self, bundle: str, tags: Optional[list] = None, version: Optional[int] = None - ) -> Optional[File]: + self, bundle: str, tags: list | None = None, version: int | None = None + ) -> File | None: """Return latest file from Housekeeper, filtered by bundle and/or tags and/or version.""" files: Query = self._store.get_files(bundle_name=bundle, tag_names=tags, version_id=version) return files.order_by(File.id.desc()).first() @@ -201,7 +198,7 @@ def check_bundle_files( bundle_name: str, file_paths: list[Path], last_version: Version, - tags: Optional[list] = None, + tags: list | None = None, ) -> list[Path]: """Checks if any of the files in the provided list are already added to the provided bundle. Returns a list of files that have not been added.""" @@ -271,7 +268,7 @@ def get_all_non_archived_spring_files(self) -> list[File]: """Return all spring files which are not marked as archived in Housekeeper.""" return self._store.get_all_non_archived_files(tag_names=[SequencingFileTag.SPRING]) - def get_latest_bundle_version(self, bundle_name: str) -> Optional[Version]: + def get_latest_bundle_version(self, bundle_name: str) -> Version | None: """Get the latest version of a Housekeeper bundle.""" last_version: Version = self.last_version(bundle_name) if not last_version: @@ -382,7 +379,7 @@ def include_files_to_latest_version(self, bundle_name: str) -> None: bundle_version.included_at = dt.datetime.now() self.commit() - def get_file_from_latest_version(self, bundle_name: str, tags: set[str]) -> Optional[File]: + def get_file_from_latest_version(self, bundle_name: str, tags: set[str]) -> File | None: """Return a file in the latest version of a bundle.""" version: Version = self.last_version(bundle=bundle_name) if not version: @@ -411,7 +408,7 @@ def is_fastq_or_spring_in_all_bundles(self, bundle_names: list[str]) -> bool: sequencing_files_in_hk[bundle_name] = False for tag in [SequencingFileTag.FASTQ, SequencingFileTag.SPRING_METADATA]: sample_file_in_hk: list[bool] = [] - hk_files: Optional[list[File]] = self.get_files_from_latest_version( + hk_files: list[File] | None = self.get_files_from_latest_version( bundle_name=bundle_name, tags=[tag] ) sample_file_in_hk += [True for hk_file in hk_files if hk_file.is_included] @@ -422,18 +419,18 @@ def is_fastq_or_spring_in_all_bundles(self, bundle_names: list[str]) -> bool: ) return all(sequencing_files_in_hk.values()) - def get_non_archived_files(self, bundle_name: str, tags: Optional[list] = None) -> list[File]: + def get_non_archived_files(self, bundle_name: str, tags: list | None = None) -> list[File]: """Returns all non-archived_files from a given bundle, tagged with the given tags""" return self._store.get_non_archived_files(bundle_name=bundle_name, tags=tags or []) - def get_archived_files(self, bundle_name: str, tags: Optional[list] = None) -> list[File]: + def get_archived_files(self, bundle_name: str, tags: list | None = None) -> list[File]: """Returns all archived_files from a given bundle, tagged with the given tags""" return self._store.get_archived_files(bundle_name=bundle_name, tags=tags or []) def add_archives(self, files: list[Path], archive_task_id: int) -> None: """Creates an archive object for the given files, and adds the archive task id to them.""" for file in files: - archived_file: Optional[File] = self._store.get_files(file_path=file.as_posix()).first() + archived_file: File | None = self._store.get_files(file_path=file.as_posix()).first() if not archived_file: raise HousekeeperFileMissingError(f"No file in housekeeper with the path {file}") archive: Archive = self._store.create_archive( @@ -451,7 +448,7 @@ def is_fastq_or_spring_on_disk_in_all_bundles(self, bundle_names: list[str]) -> sequencing_files_on_disk[bundle_name] = False for tag in [SequencingFileTag.FASTQ, SequencingFileTag.SPRING_METADATA]: sample_file_on_disk: list[bool] = [] - hk_files: Optional[list[File]] = self.get_files_from_latest_version( + hk_files: list[File] | None = self.get_files_from_latest_version( bundle_name=bundle_name, tags=[tag] ) sample_file_on_disk += [ diff --git a/cg/apps/housekeeper/models.py b/cg/apps/housekeeper/models.py index f3307fabcd..3d766a8bea 100644 --- a/cg/apps/housekeeper/models.py +++ b/cg/apps/housekeeper/models.py @@ -1,6 +1,5 @@ """Housekeeper models""" from datetime import datetime -from typing import Optional from pydantic import BaseModel @@ -14,5 +13,5 @@ class InputFile(BaseModel): class InputBundle(BaseModel): name: str created: datetime = datetime.now() - expires: Optional[datetime] = None + expires: datetime | None = None files: list[InputFile] diff --git a/cg/apps/lims/api.py b/cg/apps/lims/api.py index 8519a7b3ed..4e02127cc5 100644 --- a/cg/apps/lims/api.py +++ b/cg/apps/lims/api.py @@ -1,7 +1,6 @@ """Contains API to communicate with LIMS""" import datetime as dt import logging -from typing import Optional, Union from dateutil.parser import parse as parse_date from genologics.entities import Artifact, Process, Sample @@ -154,7 +153,7 @@ def capture_kit(self, lims_id: str) -> str: return None - def get_samples(self, *args, map_ids=False, **kwargs) -> Union[dict[str, str], list[Sample]]: + def get_samples(self, *args, map_ids=False, **kwargs) -> dict[str, str] | list[Sample]: """Bypass to original method.""" lims_samples = super(LimsAPI, self).get_samples(*args, **kwargs) if map_ids: @@ -418,7 +417,7 @@ def _get_rna_input_amounts(self, sample_id: str) -> list[tuple[dt.datetime, floa def _get_last_used_input_amount( self, input_amounts: list[tuple[dt.datetime, float]] - ) -> Optional[float]: + ) -> float | None: """Return the latest used input amount.""" sorted_input_amounts: list[tuple[dt.datetime, float]] = self._sort_by_date_run( input_amounts diff --git a/cg/apps/lims/sample_sheet.py b/cg/apps/lims/sample_sheet.py index 3d8b35cba9..048ab48537 100644 --- a/cg/apps/lims/sample_sheet.py +++ b/cg/apps/lims/sample_sheet.py @@ -1,7 +1,7 @@ """Functions to get sample sheet information from Lims.""" import logging import re -from typing import Iterable, Optional, Type +from typing import Iterable, Type from genologics.entities import Artifact, Container, Sample from genologics.lims import Lims @@ -30,7 +30,7 @@ def get_non_pooled_artifacts(artifact: Artifact) -> list[Artifact]: return artifacts -def get_reagent_label(artifact) -> Optional[str]: +def get_reagent_label(artifact) -> str | None: """Get the first and only reagent label from an artifact.""" labels: list[str] = artifact.reagent_labels if len(labels) > 1: @@ -38,7 +38,7 @@ def get_reagent_label(artifact) -> Optional[str]: return labels[0] if labels else None -def extract_sequence_in_parentheses(label: str) -> Optional[str]: +def extract_sequence_in_parentheses(label: str) -> str | None: """Return the sequence in parentheses from the reagent label or None if not found.""" match = re.match(r"^[^(]+ \(([^)]+)\)$", label) return match.group(1) if match else None @@ -83,7 +83,7 @@ def get_flow_cell_samples( non_pooled_artifacts: list[Artifact] = get_non_pooled_artifacts(placement_artifact) for artifact in non_pooled_artifacts: sample: Sample = artifact.samples[0] # we are assured it only has one sample - label: Optional[str] = get_reagent_label(artifact) + label: str | None = get_reagent_label(artifact) index = get_index(lims=lims, label=label) yield flow_cell_sample_type( flowcell_id=flow_cell_id, diff --git a/cg/apps/loqus.py b/cg/apps/loqus.py index d40daeb7b3..74dfed690b 100644 --- a/cg/apps/loqus.py +++ b/cg/apps/loqus.py @@ -2,7 +2,6 @@ import logging from pathlib import Path from subprocess import CalledProcessError -from typing import Optional from cg.constants.constants import FileFormat from cg.exc import CaseNotFoundError, LoqusdbDeleteCaseError @@ -25,14 +24,14 @@ def load( self, case_id: str, snv_vcf_path: Path, - sv_vcf_path: Optional[Path] = None, - profile_vcf_path: Optional[Path] = None, - family_ped_path: Optional[Path] = None, - window_size: Optional[int] = None, - gq_threshold: Optional[int] = None, - qual_gq: Optional[bool] = False, - hard_threshold: Optional[float] = None, - soft_threshold: Optional[float] = None, + sv_vcf_path: Path | None = None, + profile_vcf_path: Path | None = None, + family_ped_path: Path | None = None, + window_size: int | None = None, + gq_threshold: int | None = None, + qual_gq: bool | None = False, + hard_threshold: float | None = None, + soft_threshold: float | None = None, ) -> dict[str, int]: """Add observations to Loqusdb from VCF files.""" load_params = { @@ -51,7 +50,7 @@ def load( self.process.run_command(parameters=load_call_params) return self.get_nr_of_variants_in_file() - def get_case(self, case_id: str) -> Optional[dict]: + def get_case(self, case_id: str) -> dict | None: """Return a case found in Loqusdb.""" cases_parameters = ["cases", "-c", case_id, "--to-json"] self.process.run_command(parameters=cases_parameters) @@ -63,7 +62,7 @@ def get_case(self, case_id: str) -> Optional[dict]: file_format=FileFormat.JSON, stream=self.process.stdout )[0] - def get_duplicate(self, profile_vcf_path: Path, profile_threshold: float) -> Optional[dict]: + def get_duplicate(self, profile_vcf_path: Path, profile_threshold: float) -> dict | None: """Find matching profiles in Loqusdb.""" duplicates_params = { "--check-vcf": profile_vcf_path.as_posix(), diff --git a/cg/apps/orderform/excel_orderform_parser.py b/cg/apps/orderform/excel_orderform_parser.py index b0df310d86..c1aa0d5ba1 100644 --- a/cg/apps/orderform/excel_orderform_parser.py +++ b/cg/apps/orderform/excel_orderform_parser.py @@ -1,6 +1,5 @@ import logging from pathlib import Path -from typing import Optional import openpyxl from openpyxl.cell.cell import Cell @@ -70,7 +69,7 @@ def get_document_title(workbook: Workbook, orderform_sheet: Worksheet) -> str: @staticmethod def get_sample_row_info( row: tuple[Cell], header_row: list[str], empty_row_found: bool - ) -> Optional[dict]: + ) -> dict | None: """Convert an Excel row with sample data into a dict with sample info""" values = [] cell: Cell @@ -117,7 +116,7 @@ def get_raw_samples(rows: list[tuple[Cell]], header_row: list[str]) -> list[dict return raw_samples if sample_rows: - sample_dict: Optional[dict] = ExcelOrderformParser.get_sample_row_info( + sample_dict: dict | None = ExcelOrderformParser.get_sample_row_info( row=row, header_row=header_row, empty_row_found=empty_row_found ) if sample_dict: diff --git a/cg/apps/orderform/orderform_parser.py b/cg/apps/orderform/orderform_parser.py index 280c860e4f..9e84f72fe4 100644 --- a/cg/apps/orderform/orderform_parser.py +++ b/cg/apps/orderform/orderform_parser.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Hashable, Iterable, Optional +from typing import Hashable, Iterable from pydantic import BaseModel, ConfigDict, constr @@ -19,13 +19,13 @@ class OrderformParser(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) samples: list[OrderSample] = [] - project_type: Optional[OrderType] = None - delivery_type: Optional[DataDelivery] = None + project_type: OrderType | None = None + delivery_type: DataDelivery | None = None customer_id: constr( min_length=1, max_length=Customer.internal_id.property.columns[0].type.length ) = None - order_comment: Optional[str] = None - order_name: Optional[str] = None + order_comment: str | None = None + order_name: str | None = None def parse_orderform(self, orderform_file: Path) -> None: """Parse the orderform information""" diff --git a/cg/apps/scout/scout_export.py b/cg/apps/scout/scout_export.py index a8ba6f4e5b..cadc86ffa6 100644 --- a/cg/apps/scout/scout_export.py +++ b/cg/apps/scout/scout_export.py @@ -1,7 +1,6 @@ """Schemas for scout serialisation""" from datetime import datetime -from typing import Optional from pydantic import BaseModel, BeforeValidator, ConfigDict, Field from typing_extensions import Annotated, Literal @@ -22,7 +21,7 @@ class Individual(BaseModel): model_config = ConfigDict(coerce_numbers_to_str=True) - bam_file: Optional[str] = None + bam_file: str | None = None individual_id: str sex: Annotated[ Literal[PlinkGender.UNKNOWN, PlinkGender.MALE, PlinkGender.FEMALE, Gender.OTHER], @@ -52,11 +51,11 @@ class Phenotype(BaseModel): class Gene(BaseModel): model_config = ConfigDict(coerce_numbers_to_str=True) hgnc_id: int - hgnc_symbol: Optional[str] = None - region_annotation: Optional[str] = None - functional_annotation: Optional[str] = None - sift_prediction: Optional[str] = None - polyphen_prediction: Optional[str] = None + hgnc_symbol: str | None = None + region_annotation: str | None = None + functional_annotation: str | None = None + sift_prediction: str | None = None + polyphen_prediction: str | None = None class DiagnosisPhenotypes(BaseModel): @@ -64,7 +63,7 @@ class DiagnosisPhenotypes(BaseModel): disease_nr: int disease_id: str description: str - individuals: Optional[list[dict[str, str]]] = None + individuals: list[dict[str, str]] | None = None class ScoutExportCase(BaseModel): @@ -72,18 +71,18 @@ class ScoutExportCase(BaseModel): id: str = Field(str, alias="_id") analysis_date: datetime owner: str - causatives: Optional[list[str]] = None + causatives: list[str] | None = None collaborators: list[str] = [] individuals: list[Individual] genome_build: Annotated[str, BeforeValidator(convert_genome_build)] = GENOME_BUILD_37 - panels: Optional[list[Panel]] = None - rank_model_version: Optional[str] = None - sv_rank_model_version: Optional[str] = None + panels: list[Panel] | None = None + rank_model_version: str | None = None + sv_rank_model_version: str | None = None rank_score_threshold: int = 5 - phenotype_terms: Optional[list[Phenotype]] = None - phenotype_groups: Optional[list[Phenotype]] = None - diagnosis_phenotypes: Optional[list[DiagnosisPhenotypes]] = None - diagnosis_genes: Optional[list[int]] = None + phenotype_terms: list[Phenotype] | None = None + phenotype_groups: list[Phenotype] | None = None + diagnosis_phenotypes: list[DiagnosisPhenotypes] | None = None + diagnosis_genes: list[int] | None = None class Genotype(BaseModel): @@ -101,14 +100,14 @@ class Variant(BaseModel): variant_id: str chromosome: str position: int - dbsnp_id: Optional[str] = None + dbsnp_id: str | None = None reference: str alternative: str - quality: Optional[float] = None - filters: Optional[list[str]] = None + quality: float | None = None + filters: list[str] | None = None end: int rank_score: int category: str sub_category: str - genes: Optional[list[Gene]] = None + genes: list[Gene] | None = None samples: list[Genotype] diff --git a/cg/apps/scout/scoutapi.py b/cg/apps/scout/scoutapi.py index 526ccd50d0..529dcc4857 100644 --- a/cg/apps/scout/scoutapi.py +++ b/cg/apps/scout/scoutapi.py @@ -3,7 +3,6 @@ import logging from pathlib import Path from subprocess import CalledProcessError -from typing import Optional from cg.apps.scout.scout_export import ScoutExportCase, Variant from cg.constants.constants import FileFormat @@ -33,7 +32,7 @@ def upload(self, scout_load_config: Path, force: bool = False): file_format=FileFormat.YAML, file_path=scout_load_config ) scout_load_config_object: ScoutLoadConfig = ScoutLoadConfig(**scout_config) - existing_case: Optional[ScoutExportCase] = self.get_case( + existing_case: ScoutExportCase | None = self.get_case( case_id=scout_load_config_object.family ) load_command = ["load", "case", str(scout_load_config)] @@ -116,17 +115,17 @@ def get_causative_variants(self, case_id: str) -> list[Variant]: return variants - def get_case(self, case_id: str) -> Optional[ScoutExportCase]: + def get_case(self, case_id: str) -> ScoutExportCase | None: """Fetch a case from Scout""" cases: list[ScoutExportCase] = self.get_cases(case_id=case_id) return cases[0] if cases else None def get_cases( self, - case_id: Optional[str] = None, + case_id: str | None = None, reruns: bool = False, finished: bool = False, - status: Optional[str] = None, + status: str | None = None, days_ago: int = None, ) -> list[ScoutExportCase]: """Interact with cases existing in the database.""" diff --git a/cg/apps/scout/validators.py b/cg/apps/scout/validators.py index bf9f4eee08..ff80a414d9 100644 --- a/cg/apps/scout/validators.py +++ b/cg/apps/scout/validators.py @@ -1,5 +1,3 @@ -from typing import Optional - from cg.constants.gene_panel import GENOME_BUILD_37 from cg.constants.subject import Gender, PlinkGender, RelationshipStatus @@ -8,9 +6,9 @@ def convert_genome_build(value): return GENOME_BUILD_37 if value is None else value -def set_parent_if_missing(parent: Optional[str]) -> str: +def set_parent_if_missing(parent: str | None) -> str: return RelationshipStatus.HAS_NO_PARENT if parent is None else parent -def set_gender_if_other(gender: Optional[str]) -> str: +def set_gender_if_other(gender: str | None) -> str: return PlinkGender.UNKNOWN if gender == Gender.OTHER else gender diff --git a/cg/apps/sequencing_metrics_parser/models/bcl2fastq_metrics.py b/cg/apps/sequencing_metrics_parser/models/bcl2fastq_metrics.py index ad883b2315..d47ae70a63 100644 --- a/cg/apps/sequencing_metrics_parser/models/bcl2fastq_metrics.py +++ b/cg/apps/sequencing_metrics_parser/models/bcl2fastq_metrics.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import BaseModel, Field @@ -33,7 +31,7 @@ class ConversionResult(BaseModel): lane: int = Field(..., alias="LaneNumber", gt=0) tile_reads: list[TileReads] = Field(..., alias="DemuxResults") - tile_undetermined_reads: Optional[UndeterminedTileReads] = Field(None, alias="Undetermined") + tile_undetermined_reads: UndeterminedTileReads | None = Field(None, alias="Undetermined") class SampleLaneTileMetrics(BaseModel): diff --git a/cg/apps/sequencing_metrics_parser/parsers/bcl2fastq.py b/cg/apps/sequencing_metrics_parser/parsers/bcl2fastq.py index 0dc15c7ad3..fe8239cd38 100644 --- a/cg/apps/sequencing_metrics_parser/parsers/bcl2fastq.py +++ b/cg/apps/sequencing_metrics_parser/parsers/bcl2fastq.py @@ -1,7 +1,7 @@ import logging import os from pathlib import Path -from typing import Iterable, Optional +from typing import Iterable from cg.apps.sequencing_metrics_parser.models.bcl2fastq_metrics import ( ReadMetric, @@ -151,7 +151,7 @@ def get_metrics_for_non_pooled_samples( """Get metrics for non pooled samples and set sample ids.""" non_pooled_metrics: list[SampleLaneMetrics] = [] for lane, sample_id in non_pooled_lane_sample_pairs: - metric: Optional[SampleLaneMetrics] = lane_metrics.get(lane) + metric: SampleLaneMetrics | None = lane_metrics.get(lane) if not metric: continue metric.sample_id = sample_id diff --git a/cg/apps/sequencing_metrics_parser/parsers/bcl_convert.py b/cg/apps/sequencing_metrics_parser/parsers/bcl_convert.py index 846ecdfbd8..b14feee6fb 100644 --- a/cg/apps/sequencing_metrics_parser/parsers/bcl_convert.py +++ b/cg/apps/sequencing_metrics_parser/parsers/bcl_convert.py @@ -1,7 +1,7 @@ """This module parses metrics for files generated by the BCLConvert tool using the Dragen hardware.""" import logging from pathlib import Path -from typing import Callable, Optional, Union +from typing import Callable from cg.apps.demultiplex.sample_sheet.validators import is_valid_sample_internal_id from cg.apps.sequencing_metrics_parser.models.bcl_convert import ( @@ -48,10 +48,10 @@ def __init__( def parse_metrics_file( self, metrics_file_path, metrics_model: Callable - ) -> list[Union[BclConvertQualityMetrics, BclConvertDemuxMetrics]]: + ) -> list[BclConvertQualityMetrics | BclConvertDemuxMetrics]: """Parse specified BCL convert metrics file.""" LOG.info(f"Parsing BCLConvert metrics file: {metrics_file_path}") - parsed_metrics: list[Union[BclConvertQualityMetrics, BclConvertDemuxMetrics]] = [] + parsed_metrics: list[BclConvertQualityMetrics | BclConvertDemuxMetrics] = [] metrics_content: list[dict] = ReadFile.get_content_from_file( file_format=FileFormat.CSV, file_path=metrics_file_path, read_to_dict=True ) @@ -79,10 +79,10 @@ def get_lanes_for_sample(self, sample_internal_id: str) -> list[int]: def get_metrics_for_sample_and_lane( self, - metrics: list[Union[BclConvertQualityMetrics, BclConvertDemuxMetrics]], + metrics: list[BclConvertQualityMetrics | BclConvertDemuxMetrics], sample_internal_id: str, lane: int, - ) -> Union[BclConvertQualityMetrics, BclConvertDemuxMetrics]: + ) -> BclConvertQualityMetrics | BclConvertDemuxMetrics: """Return the metrics for a sample by sample internal id.""" for metric in metrics: if metric.sample_internal_id == sample_internal_id and metric.lane == lane: @@ -141,7 +141,7 @@ def get_mean_quality_score_for_sample_in_lane( def has_undetermined_reads_in_lane(self, lane: int) -> bool: """Return whether there are undetermined reads in a lane.""" - metrics: Optional[BclConvertQualityMetrics] = self.get_metrics_for_sample_and_lane( + metrics: BclConvertQualityMetrics | None = self.get_metrics_for_sample_and_lane( metrics=self.quality_metrics, sample_internal_id=UNDETERMINED, lane=lane ) return bool(metrics) diff --git a/cg/apps/slurm/slurm_api.py b/cg/apps/slurm/slurm_api.py index 18faae5a97..78ec8b6974 100644 --- a/cg/apps/slurm/slurm_api.py +++ b/cg/apps/slurm/slurm_api.py @@ -1,7 +1,6 @@ """Module to create sbatch files and communicate with SLURM.""" import logging from pathlib import Path -from typing import Optional from cg.apps.slurm.sbatch import ( DRAGEN_SBATCH_HEADER_TEMPLATE, @@ -53,7 +52,7 @@ def generate_dragen_sbatch_header(sbatch_parameters: Sbatch) -> str: return DRAGEN_SBATCH_HEADER_TEMPLATE.format(**sbatch_parameters.model_dump()) @staticmethod - def generate_sbatch_body(commands: str, error_function: Optional[str] = None) -> str: + def generate_sbatch_body(commands: str, error_function: str | None = None) -> str: if not error_function: error_function = "log 'Something went wrong, aborting'" diff --git a/cg/apps/tb/api.py b/cg/apps/tb/api.py index dcf3ebb06e..b4c836b2eb 100644 --- a/cg/apps/tb/api.py +++ b/cg/apps/tb/api.py @@ -1,7 +1,7 @@ """ Trailblazer API for cg.""" import datetime import logging -from typing import Any, Optional +from typing import Any from google.auth import jwt from google.auth.crypt import RSASigner @@ -66,7 +66,7 @@ def query_trailblazer( LOG.debug(f"RESPONSE BODY {response.text}") return ReadStream.get_content_from_stream(file_format=FileFormat.JSON, stream=response.text) - def get_latest_analysis(self, case_id: str) -> Optional[TrailblazerAnalysis]: + def get_latest_analysis(self, case_id: str) -> TrailblazerAnalysis | None: request_body = { "case_id": case_id, } @@ -74,7 +74,7 @@ def get_latest_analysis(self, case_id: str) -> Optional[TrailblazerAnalysis]: if response: return TrailblazerAnalysis.model_validate(response) - def get_latest_analysis_status(self, case_id: str) -> Optional[str]: + def get_latest_analysis_status(self, case_id: str) -> str | None: latest_analysis = self.get_latest_analysis(case_id=case_id) if latest_analysis: return latest_analysis.status @@ -91,7 +91,7 @@ def is_latest_analysis_completed(self, case_id: str) -> bool: def is_latest_analysis_qc(self, case_id: str) -> bool: return self.get_latest_analysis_status(case_id=case_id) == AnalysisStatus.QC - def mark_analyses_deleted(self, case_id: str) -> Optional[list]: + def mark_analyses_deleted(self, case_id: str) -> list | None: """Mark all analyses for case deleted without removing analysis files""" request_body = { "case_id": case_id, diff --git a/cg/apps/tb/models.py b/cg/apps/tb/models.py index d20166a115..d37dd1b921 100644 --- a/cg/apps/tb/models.py +++ b/cg/apps/tb/models.py @@ -1,6 +1,5 @@ import datetime as dt from pathlib import Path -from typing import Optional from pydantic import ( BaseModel, @@ -17,30 +16,30 @@ class TrailblazerAnalysis(BaseModel): id: int family: str - case_id: Optional[str] = None + case_id: str | None = None @field_validator("case_id") @classmethod def inherit_family_value(cls, value: str, info: FieldValidationInfo) -> str: return info.data.get("family") - version: Optional[str] = None - logged_at: Annotated[Optional[dt.datetime], BeforeValidator(parse_str_to_datetime)] - started_at: Annotated[Optional[dt.datetime], BeforeValidator(parse_str_to_datetime)] - completed_at: Annotated[Optional[dt.datetime], BeforeValidator(parse_str_to_datetime)] - status: Optional[str] = None - priority: Optional[str] = None - out_dir: Annotated[Optional[Path], BeforeValidator(parse_str_to_path)] - config_path: Annotated[Optional[Path], BeforeValidator(parse_str_to_path)] - comment: Optional[str] = None - is_deleted: Optional[bool] = None - is_visible: Optional[bool] = None - type: Optional[str] = None - user_id: Optional[int] = None - progress: Optional[float] = 0.0 - data_analysis: Optional[str] = None - ticket: Optional[str] = None - uploaded_at: Optional[str] = None + version: str | None = None + logged_at: Annotated[dt.datetime | None, BeforeValidator(parse_str_to_datetime)] + started_at: Annotated[dt.datetime | None, BeforeValidator(parse_str_to_datetime)] + completed_at: Annotated[dt.datetime | None, BeforeValidator(parse_str_to_datetime)] + status: str | None = None + priority: str | None = None + out_dir: Annotated[Path | None, BeforeValidator(parse_str_to_path)] + config_path: Annotated[Path | None, BeforeValidator(parse_str_to_path)] + comment: str | None = None + is_deleted: bool | None = None + is_visible: bool | None = None + type: str | None = None + user_id: int | None = None + progress: float | None = 0.0 + data_analysis: str | None = None + ticket: str | None = None + uploaded_at: str | None = None model_config = ConfigDict( arbitrary_types_allowed=True, diff --git a/cg/apps/tb/validators.py b/cg/apps/tb/validators.py index 56f0e56aac..6f0f301d50 100644 --- a/cg/apps/tb/validators.py +++ b/cg/apps/tb/validators.py @@ -1,15 +1,14 @@ import datetime as dt from pathlib import Path -from typing import Optional from dateutil.parser import parse as parse_datestr -def parse_str_to_datetime(value: str) -> Optional[dt.datetime]: +def parse_str_to_datetime(value: str) -> dt.datetime | None: if value: return parse_datestr(value) -def parse_str_to_path(value: str) -> Optional[Path]: +def parse_str_to_path(value: str) -> Path | None: if value: return Path(value) diff --git a/cg/cli/add.py b/cg/cli/add.py index 3f699b3f6f..bfb228d51c 100644 --- a/cg/cli/add.py +++ b/cg/cli/add.py @@ -1,5 +1,4 @@ import logging -from typing import Optional import click @@ -79,7 +78,7 @@ def add_customer( context: CGConfig, internal_id: str, name: str, - collaboration_internal_ids: Optional[list[str]], + collaboration_internal_ids: list[str] | None, invoice_address: str, invoice_reference: str, data_archive_location: str, @@ -173,10 +172,10 @@ def add_user(context: CGConfig, admin: bool, customer_id: str, email: str, name: @click.pass_obj def add_sample( context: CGConfig, - lims_id: Optional[str], - down_sampled: Optional[int], + lims_id: str | None, + down_sampled: int | None, sex: Gender, - order: Optional[str], + order: str | None, application_tag: str, priority: Priority, customer_id: str, @@ -288,16 +287,16 @@ def add_case( @click.pass_obj def link_sample_to_case( context: CGConfig, - mother_id: Optional[str], - father_id: Optional[str], + mother_id: str | None, + father_id: str | None, status: str, case_id: str, sample_id: str, ): """Create a link between a case id and a sample id.""" status_db: Store = context.status_db - mother: Optional[Sample] = None - father: Optional[Sample] = None + mother: Sample | None = None + father: Sample | None = None case_obj: Case = status_db.get_case_by_internal_id(internal_id=case_id) if case_obj is None: LOG.error(f"{case_id}: family not found") diff --git a/cg/cli/backup.py b/cg/cli/backup.py index 22febac647..d96361cf70 100644 --- a/cg/cli/backup.py +++ b/cg/cli/backup.py @@ -1,7 +1,7 @@ """Backup related CLI commands.""" import logging from pathlib import Path -from typing import Iterable, Optional, Union +from typing import Iterable import click import housekeeper.store.models as hk_models @@ -55,9 +55,7 @@ def backup_flow_cells(context: CGConfig, dry_run: bool): flow_cells_dir=Path(context.flow_cells_dir) ) for flow_cell in flow_cells: - db_flow_cell: Optional[Flowcell] = status_db.get_flow_cell_by_name( - flow_cell_name=flow_cell.id - ) + db_flow_cell: Flowcell | None = status_db.get_flow_cell_by_name(flow_cell_name=flow_cell.id) flow_cell_encryption_api = FlowCellEncryptionAPI( binary_path=context.encryption.binary_path, dry_run=dry_run, @@ -93,9 +91,7 @@ def encrypt_flow_cells(context: CGConfig, dry_run: bool): flow_cells_dir=Path(context.flow_cells_dir) ) for flow_cell in flow_cells: - db_flow_cell: Optional[Flowcell] = status_db.get_flow_cell_by_name( - flow_cell_name=flow_cell.id - ) + db_flow_cell: Flowcell | None = status_db.get_flow_cell_by_name(flow_cell_name=flow_cell.id) if db_flow_cell and db_flow_cell.has_backup: LOG.debug(f"Flow cell: {flow_cell.id} is already backed-up") continue @@ -119,7 +115,7 @@ def encrypt_flow_cells(context: CGConfig, dry_run: bool): @click.option("-f", "--flow-cell-id", help="Retrieve a specific flow cell, ex. 'HCK2KDSXX'") @DRY_RUN @click.pass_obj -def fetch_flow_cell(context: CGConfig, dry_run: bool, flow_cell_id: Optional[str] = None): +def fetch_flow_cell(context: CGConfig, dry_run: bool, flow_cell_id: str | None = None): """Fetch the first flow cell in the requested queue from backup""" pdc_api = context.pdc_api @@ -138,7 +134,7 @@ def fetch_flow_cell(context: CGConfig, dry_run: bool, flow_cell_id: Optional[str backup_api: BackupAPI = context.meta_apis["backup_api"] status_api: Store = context.status_db - flow_cell: Optional[Flowcell] = ( + flow_cell: Flowcell | None = ( status_api.get_flow_cell_by_name(flow_cell_name=flow_cell_id) if flow_cell_id else None ) @@ -149,7 +145,7 @@ def fetch_flow_cell(context: CGConfig, dry_run: bool, flow_cell_id: Optional[str if not flow_cell_id: LOG.info("Fetching first flow cell in queue") - retrieval_time: Optional[float] = backup_api.fetch_flow_cell(flow_cell=flow_cell) + retrieval_time: float | None = backup_api.fetch_flow_cell(flow_cell=flow_cell) if retrieval_time: hours = retrieval_time / 60 / 60 @@ -244,7 +240,7 @@ def _get_samples(status_api: Store, object_type: str, identifier: str) -> list[S "case": status_api.get_samples_by_case_id, "flow_cell": status_api.get_samples_from_flow_cell, } - samples: Union[Sample, list[Sample]] = get_samples[object_type](identifier) + samples: Sample | list[Sample] = get_samples[object_type](identifier) return samples if isinstance(samples, list) else [samples] diff --git a/cg/cli/base.py b/cg/cli/base.py index 3df995be1f..e38df62c6f 100644 --- a/cg/cli/base.py +++ b/cg/cli/base.py @@ -2,7 +2,6 @@ import logging import sys from pathlib import Path -from typing import List, Optional import click import coloredlogs @@ -57,7 +56,7 @@ def teardown_session(): def base( context: click.Context, config: click.Path, - database: Optional[str], + database: str | None, log_level: str, verbose: bool, ): @@ -83,7 +82,7 @@ def base( @click.pass_obj def init(context: CGConfig, reset: bool, force: bool): """Setup the database.""" - existing_tables: List[str] = get_tables() + existing_tables: list[str] = get_tables() if force or reset: if existing_tables and not force: message = f"Delete existing tables? [{', '.join(existing_tables)}]" diff --git a/cg/cli/clean.py b/cg/cli/clean.py index dcc208ebc4..59061f422a 100644 --- a/cg/cli/clean.py +++ b/cg/cli/clean.py @@ -2,7 +2,6 @@ import logging from datetime import datetime, timedelta from pathlib import Path -from typing import Optional import click from housekeeper.store.models import File, Version @@ -179,10 +178,10 @@ def hk_case_bundle_files(context: CGConfig, days_old: int, dry_run: bool = False @click.pass_obj def hk_bundle_files( context: CGConfig, - case_id: Optional[str], + case_id: str | None, tags: list, - days_old: Optional[int], - pipeline: Optional[Pipeline], + days_old: int | None, + pipeline: Pipeline | None, dry_run: bool, ): """Remove files found in Housekeeper bundles.""" @@ -211,7 +210,7 @@ def hk_bundle_files( for analysis in analyses: LOG.info(f"Cleaning analysis {analysis}") bundle_name: str = analysis.case.internal_id - hk_bundle_version: Optional[Version] = housekeeper_api.version( + hk_bundle_version: Version | None = housekeeper_api.version( bundle=bundle_name, date=analysis.started_at ) if not hk_bundle_version: diff --git a/cg/cli/compress/fastq.py b/cg/cli/compress/fastq.py index 5d530c134f..fc5321e317 100644 --- a/cg/cli/compress/fastq.py +++ b/cg/cli/compress/fastq.py @@ -1,7 +1,7 @@ """CLI function to compress FASTQ files into SPRING archives.""" import logging -from typing import Iterable, Optional +from typing import Iterable import click @@ -39,12 +39,12 @@ @click.pass_obj def fastq_cmd( context: CGConfig, - case_id: Optional[str], + case_id: str | None, days_back: int, - hours: Optional[int], + hours: int | None, dry_run: bool, - mem: Optional[int], - ntasks: Optional[int], + mem: int | None, + ntasks: int | None, number_of_conversions: int, ): """Compress old FASTQ files into SPRING.""" @@ -77,7 +77,7 @@ def fastq_cmd( ) @DRY_RUN @click.pass_obj -def clean_fastq(context: CGConfig, case_id: Optional[str], days_back: int, dry_run: bool): +def clean_fastq(context: CGConfig, case_id: str | None, days_back: int, dry_run: bool): """Remove compressed FASTQ files, and update links in Housekeeper to SPRING files.""" LOG.info("Running compress clean FASTQ") compress_api: CompressAPI = context.meta_apis["compress_api"] @@ -105,7 +105,7 @@ def clean_fastq(context: CGConfig, case_id: Optional[str], days_back: int, dry_r @click.option("-b", "--bundle-name") @DRY_RUN @click.pass_obj -def fix_spring(context: CGConfig, bundle_name: Optional[str], dry_run: bool): +def fix_spring(context: CGConfig, bundle_name: str | None, dry_run: bool): """Check if bundle(s) have non-existing SPRING files and correct these.""" LOG.info("Running fix spring") compress_api = context.meta_apis["compress_api"] diff --git a/cg/cli/compress/helpers.py b/cg/cli/compress/helpers.py index 710e92a3bf..135d452116 100644 --- a/cg/cli/compress/helpers.py +++ b/cg/cli/compress/helpers.py @@ -4,7 +4,7 @@ import os from math import ceil from pathlib import Path -from typing import Iterator, Optional +from typing import Iterator from housekeeper.store.models import Bundle, Version @@ -21,7 +21,7 @@ LOG = logging.getLogger(__name__) -def get_cases_to_process(days_back: int, store: Store, case_id: Optional[str] = None) -> list[Case]: +def get_cases_to_process(days_back: int, store: Store, case_id: str | None = None) -> list[Case]: """Return cases to process.""" cases: list[Case] = [] if case_id: @@ -49,8 +49,8 @@ def get_fastq_individuals(store: Store, case_id: str = None) -> Iterator[str]: def set_memory_according_to_reads( - sample_id: str, sample_reads: Optional[int] = None, sample_process_mem: Optional[int] = None -) -> Optional[int]: + sample_id: str, sample_reads: int | None = None, sample_process_mem: int | None = None +) -> int | None: """Set SLURM sample process memory depending on number of sample reads if sample_process_mem is not set.""" if sample_process_mem: return sample_process_mem @@ -108,7 +108,7 @@ def get_versions(hk_api: HousekeeperAPI, bundle_name: str = None) -> Iterator[Ve yield last_version -def get_true_dir(dir_path: Path) -> Optional[Path]: +def get_true_dir(dir_path: Path) -> Path | None: """Loop over the files in a directory, if any symlinks are found return the parent dir of the origin file.""" # Check if there are any links to fastq files in the directory @@ -142,7 +142,7 @@ def compress_sample_fastqs_in_cases( if not case.links: continue for case_link in case.links: - sample_process_mem: Optional[int] = set_memory_according_to_reads( + sample_process_mem: int | None = set_memory_according_to_reads( sample_process_mem=mem, sample_id=case_link.sample.internal_id, sample_reads=case_link.sample.reads, diff --git a/cg/cli/delete/observations.py b/cg/cli/delete/observations.py index 9d6c3738c4..7ec628893e 100644 --- a/cg/cli/delete/observations.py +++ b/cg/cli/delete/observations.py @@ -1,7 +1,6 @@ """Delete observations CLI.""" import logging -from typing import Optional, Union import click from sqlalchemy.orm import Query @@ -31,7 +30,7 @@ def delete_observations(context: CGConfig, case_id: str, dry_run: bool, yes: boo """Delete a case from Loqusdb and reset the Loqusdb IDs in StatusDB.""" case: Case = get_observations_case(context, case_id, upload=False) - observations_api: Union[MipDNAObservationsAPI, BalsamicObservationsAPI] = get_observations_api( + observations_api: MipDNAObservationsAPI | BalsamicObservationsAPI = get_observations_api( context, case ) @@ -50,7 +49,7 @@ def delete_observations(context: CGConfig, case_id: str, dry_run: bool, yes: boo @DRY_RUN @click.pass_context def delete_available_observations( - context: click.Context, pipeline: Optional[Pipeline], dry_run: bool, yes: bool + context: click.Context, pipeline: Pipeline | None, dry_run: bool, yes: bool ): """Delete available observation from Loqusdb.""" diff --git a/cg/cli/deliver/base.py b/cg/cli/deliver/base.py index 0b4191bbc4..1404b35b47 100644 --- a/cg/cli/deliver/base.py +++ b/cg/cli/deliver/base.py @@ -1,7 +1,6 @@ """CLI for delivering files with CG""" import logging from pathlib import Path -from typing import Optional import click @@ -61,8 +60,8 @@ def deliver(): @click.pass_obj def deliver_analysis( context: CGConfig, - case_id: Optional[str], - ticket: Optional[str], + case_id: str | None, + ticket: str | None, delivery_type: list[str], dry_run: bool, force_all: bool, diff --git a/cg/cli/demultiplex/copy_novaseqx_demultiplex_data.py b/cg/cli/demultiplex/copy_novaseqx_demultiplex_data.py index 5494ea90f7..87a003d131 100644 --- a/cg/cli/demultiplex/copy_novaseqx_demultiplex_data.py +++ b/cg/cli/demultiplex/copy_novaseqx_demultiplex_data.py @@ -2,7 +2,6 @@ import os import shutil from pathlib import Path -from typing import Optional from cg.constants.demultiplexing import DemultiplexingDirsAndFiles @@ -28,7 +27,7 @@ def is_flow_cell_in_demultiplexed_runs(flow_cell_name: str, demultiplexed_runs: return Path(demultiplexed_runs, flow_cell_name).exists() -def get_latest_analysis_path(flow_cell_dir: Path) -> Optional[Path]: +def get_latest_analysis_path(flow_cell_dir: Path) -> Path | None: """ Get the latest analysis directory for a Novaseqx flow cell. The latest analysis directory is the one with the highest integer name. diff --git a/cg/cli/demultiplex/sample_sheet.py b/cg/cli/demultiplex/sample_sheet.py index 512873ec6b..70e223b141 100644 --- a/cg/cli/demultiplex/sample_sheet.py +++ b/cg/cli/demultiplex/sample_sheet.py @@ -1,7 +1,6 @@ import logging import os from pathlib import Path -from typing import Optional import click from pydantic import ValidationError @@ -152,7 +151,7 @@ def create_all_sheets(context: CGConfig, dry_run: bool): flow_cell_id: str = flow_cell.id try: - sample_sheet_path: Optional[Path] = hk_api.get_sample_sheet_path(flow_cell_id) + sample_sheet_path: Path | None = hk_api.get_sample_sheet_path(flow_cell_id) except HousekeeperFileMissingError: sample_sheet_path = None diff --git a/cg/cli/generate/report/base.py b/cg/cli/generate/report/base.py index 5ac4e1f5fa..470e457090 100644 --- a/cg/cli/generate/report/base.py +++ b/cg/cli/generate/report/base.py @@ -3,7 +3,6 @@ import sys from datetime import datetime from pathlib import Path -from typing import Optional import click from housekeeper.store.models import Version @@ -57,7 +56,7 @@ def generate_delivery_report( return version: Version = report_api.housekeeper_api.version(bundle=case_id, date=analysis_date) - delivery_report: Optional[str] = report_api.get_delivery_report_from_hk( + delivery_report: str | None = report_api.get_delivery_report_from_hk( case_id=case_id, version=version ) if delivery_report: diff --git a/cg/cli/generate/report/utils.py b/cg/cli/generate/report/utils.py index dc02b7b3b7..fb3af79776 100644 --- a/cg/cli/generate/report/utils.py +++ b/cg/cli/generate/report/utils.py @@ -1,7 +1,6 @@ """Delivery report helpers.""" import logging from datetime import datetime -from typing import Optional import click @@ -101,7 +100,7 @@ def get_report_api_pipeline(context: click.Context, pipeline: Pipeline) -> Repor def get_report_analysis_started( - case: Case, report_api: ReportAPI, analysis_started_at: Optional[str] + case: Case, report_api: ReportAPI, analysis_started_at: str | None ) -> datetime: """Resolves and returns a valid analysis date.""" if not analysis_started_at: diff --git a/cg/cli/get.py b/cg/cli/get.py index 0e875bc58a..114e9b7f2b 100644 --- a/cg/cli/get.py +++ b/cg/cli/get.py @@ -1,6 +1,6 @@ import logging import re -from typing import Iterable, Optional +from typing import Iterable import click from tabulate import tabulate @@ -20,7 +20,7 @@ @click.group(invoke_without_command=True) @click.option("-i", "--identifier", help="made a guess what type you are looking for") @click.pass_context -def get(context: click.Context, identifier: Optional[str]): +def get(context: click.Context, identifier: str | None): """Get information about records in the database.""" if identifier: if re.match(r"^[A-Z]{3}[0-9]{4,5}[A-Z]{1}[1-9]{1,3}$", identifier): diff --git a/cg/cli/set/base.py b/cg/cli/set/base.py index 8f7f45bd73..69987bc4e2 100644 --- a/cg/cli/set/base.py +++ b/cg/cli/set/base.py @@ -2,7 +2,7 @@ import datetime import getpass import logging -from typing import Iterable, Optional +from typing import Iterable import click @@ -139,7 +139,7 @@ def is_private_attribute(key: str) -> bool: def list_changeable_sample_attributes( - sample: Optional[Sample] = None, skip_attributes: list[str] = [] + sample: Sample | None = None, skip_attributes: list[str] = [] ) -> None: """List changeable attributes on sample and its current value.""" LOG.info("Below is a set of changeable sample attributes, to combine with -kv flag:\n") @@ -159,7 +159,7 @@ def list_changeable_sample_attributes( @click.pass_obj def list_keys( context: CGConfig, - sample_id: Optional[str], + sample_id: str | None, ): """List all available modifiable keys.""" status_db: Store = context.status_db @@ -185,7 +185,7 @@ def list_keys( @click.pass_obj def sample( context: CGConfig, - sample_id: Optional[str], + sample_id: str | None, kwargs: click.Tuple([str, str]), skip_lims: bool, yes: bool, @@ -286,7 +286,7 @@ def _update_comment(comment, obj): @click.option("-s", "--status", type=click.Choice(FLOWCELL_STATUS)) @click.argument("flow_cell_name") @click.pass_obj -def flowcell(context: CGConfig, flow_cell_name: str, status: Optional[str]): +def flowcell(context: CGConfig, flow_cell_name: str, status: str | None): """Update information about a flow cell.""" status_db: Store = context.status_db flowcell_obj: Flowcell = status_db.get_flow_cell_by_name(flow_cell_name=flow_cell_name) diff --git a/cg/cli/set/case.py b/cg/cli/set/case.py index 62de913342..a4a73d9c9d 100644 --- a/cg/cli/set/case.py +++ b/cg/cli/set/case.py @@ -1,6 +1,5 @@ """Set case attributes in the status database.""" import logging -from typing import Optional import click @@ -38,13 +37,13 @@ @click.pass_obj def set_case( context: CGConfig, - action: Optional[str], - data_analysis: Optional[Pipeline], - data_delivery: Optional[DataDelivery], - priority: Optional[Priority], - panel_abbreviations: Optional[tuple[str]], + action: str | None, + data_analysis: Pipeline | None, + data_delivery: DataDelivery | None, + priority: Priority | None, + panel_abbreviations: tuple[str] | None, case_id: str, - customer_id: Optional[str], + customer_id: str | None, ): """Update information about a case.""" diff --git a/cg/cli/set/cases.py b/cg/cli/set/cases.py index 62d7dba0f9..cdf388aeb8 100644 --- a/cg/cli/set/cases.py +++ b/cg/cli/set/cases.py @@ -1,5 +1,4 @@ import logging -from typing import Optional import click @@ -50,10 +49,10 @@ def _get_cases(identifiers: click.Tuple([str, str]), store: Store) -> list[Case] @click.pass_context def set_cases( context: click.Context, - action: Optional[str], - priority: Optional[Priority], - panel_abbreviations: Optional[tuple[str]], - customer_id: Optional[str], + action: str | None, + priority: Priority | None, + panel_abbreviations: tuple[str] | None, + customer_id: str | None, identifiers: click.Tuple([str, str]), ): """Set values on many families at the same time""" diff --git a/cg/cli/upload/base.py b/cg/cli/upload/base.py index 87db73a0c4..e1cb57c8d3 100644 --- a/cg/cli/upload/base.py +++ b/cg/cli/upload/base.py @@ -2,7 +2,6 @@ import logging import sys import traceback -from typing import Optional import click @@ -54,7 +53,7 @@ help="Force upload of an analysis that has already been uploaded or marked as started", ) @click.pass_context -def upload(context: click.Context, case_id: Optional[str], restart: bool): +def upload(context: click.Context, case_id: str | None, restart: bool): """Upload results from analyses""" config_object: CGConfig = context.obj diff --git a/cg/cli/upload/delivery_report.py b/cg/cli/upload/delivery_report.py index 4effc89347..441708e851 100644 --- a/cg/cli/upload/delivery_report.py +++ b/cg/cli/upload/delivery_report.py @@ -1,6 +1,5 @@ """Delivery report upload to scout commands.""" import logging -from typing import Optional import click from housekeeper.store.models import Version @@ -30,7 +29,7 @@ def upload_delivery_report_to_scout( case: Case = get_report_case(context, case_id) report_api: ReportAPI = get_report_api(context, case) version: Version = report_api.housekeeper_api.last_version(case_id) - delivery_report: Optional[str] = report_api.get_delivery_report_from_hk( + delivery_report: str | None = report_api.get_delivery_report_from_hk( case_id=case_id, version=version ) if delivery_report and not dry_run: diff --git a/cg/cli/upload/fohm.py b/cg/cli/upload/fohm.py index 73fb50efc9..c017e90cc4 100644 --- a/cg/cli/upload/fohm.py +++ b/cg/cli/upload/fohm.py @@ -1,5 +1,4 @@ import logging -from typing import Optional import click @@ -39,7 +38,7 @@ def fohm(context: CGConfig): @ARGUMENT_DATE @click.pass_obj def aggregate_delivery( - context: CGConfig, cases: list, dry_run: bool = False, datestr: Optional[str] = None + context: CGConfig, cases: list, dry_run: bool = False, datestr: str | None = None ): """Re-aggregates delivery files for FOHM and saves them to default working directory""" fohm_api = FOHMUploadAPI(config=context, dry_run=dry_run, datestr=datestr) @@ -57,7 +56,7 @@ def aggregate_delivery( @ARGUMENT_DATE @click.pass_obj def create_komplettering( - context: CGConfig, cases: list, dry_run: bool = False, datestr: Optional[str] = None + context: CGConfig, cases: list, dry_run: bool = False, datestr: str | None = None ): """Re-aggregates komplettering files for FOHM and saves them to default working directory""" fohm_api = FOHMUploadAPI(config=context, dry_run=dry_run, datestr=datestr) @@ -73,7 +72,7 @@ def create_komplettering( @ARGUMENT_DATE @click.pass_obj def preprocess_all( - context: CGConfig, cases: list, dry_run: bool = False, datestr: Optional[str] = None + context: CGConfig, cases: list, dry_run: bool = False, datestr: str | None = None ): """Create all FOHM upload files, upload to GISAID, sync SFTP and mail reports for all provided cases""" fohm_api = FOHMUploadAPI(config=context, dry_run=dry_run, datestr=datestr) @@ -108,7 +107,7 @@ def preprocess_all( @ARGUMENT_DATE @OPTION_DRY_RUN @click.pass_obj -def upload_rawdata(context: CGConfig, dry_run: bool = False, datestr: Optional[str] = None): +def upload_rawdata(context: CGConfig, dry_run: bool = False, datestr: str | None = None): """Deliver files in daily upload directory via sftp""" fohm_api = FOHMUploadAPI(config=context, dry_run=dry_run, datestr=datestr) fohm_api.sync_files_sftp() @@ -118,7 +117,7 @@ def upload_rawdata(context: CGConfig, dry_run: bool = False, datestr: Optional[s @ARGUMENT_DATE @OPTION_DRY_RUN @click.pass_obj -def send_reports(context: CGConfig, dry_run: bool = False, datestr: Optional[str] = None): +def send_reports(context: CGConfig, dry_run: bool = False, datestr: str | None = None): """Send all komplettering reports found in current daily directory to target recipients""" fohm_api = FOHMUploadAPI(config=context, dry_run=dry_run, datestr=datestr) fohm_api.send_mail_reports() diff --git a/cg/cli/upload/genotype.py b/cg/cli/upload/genotype.py index 8549022b0f..6cb6a3263b 100644 --- a/cg/cli/upload/genotype.py +++ b/cg/cli/upload/genotype.py @@ -1,6 +1,5 @@ """Code for uploading genotype data via CLI""" import logging -from typing import Optional import click @@ -25,7 +24,7 @@ ) @click.argument("family_id", required=False) @click.pass_obj -def upload_genotypes(context: CGConfig, re_upload: bool, family_id: Optional[str]): +def upload_genotypes(context: CGConfig, re_upload: bool, family_id: str | None): """Upload genotypes from an analysis to Genotype.""" status_db: Store = context.status_db diff --git a/cg/cli/upload/gens.py b/cg/cli/upload/gens.py index 1af0dd1b79..30d524bae2 100644 --- a/cg/cli/upload/gens.py +++ b/cg/cli/upload/gens.py @@ -1,6 +1,5 @@ """Module for uploading to Gens via CLI.""" import logging -from typing import Optional import click from housekeeper.store.models import File @@ -22,7 +21,7 @@ @ARGUMENT_CASE_ID @OPTION_DRY @click.pass_obj -def upload_to_gens(context: CGConfig, case_id: Optional[str], dry_run: bool): +def upload_to_gens(context: CGConfig, case_id: str | None, dry_run: bool): """Upload data from an analysis to Gens.""" click.echo(click.style("----------------- GENS -------------------")) diff --git a/cg/cli/upload/mutacc.py b/cg/cli/upload/mutacc.py index e75ac0febc..63b188fda0 100644 --- a/cg/cli/upload/mutacc.py +++ b/cg/cli/upload/mutacc.py @@ -1,6 +1,5 @@ """Code that handles uploading to mutacc from the CLI""" import logging -from typing import Optional import click @@ -20,7 +19,7 @@ @click.option("--dry-run", is_flag=True, help="only print cases to be processed") @click.pass_obj def process_solved( - context: CGConfig, case_id: Optional[str], days_ago: int, customers: tuple[str], dry_run: bool + context: CGConfig, case_id: str | None, days_ago: int, customers: tuple[str], dry_run: bool ): """Process cases with mutacc that has been marked as solved in scout. This prepares them to be uploaded to the mutacc database""" diff --git a/cg/cli/upload/nipt/base.py b/cg/cli/upload/nipt/base.py index 34583921d7..9133779b4f 100644 --- a/cg/cli/upload/nipt/base.py +++ b/cg/cli/upload/nipt/base.py @@ -2,7 +2,6 @@ import logging import traceback -from typing import Optional import click @@ -29,7 +28,7 @@ def nipt(): @click.option("--dry-run", is_flag=True) @click.option("--force", is_flag=True, help="Force upload of case to databases, despite qc") @click.pass_context -def nipt_upload_case(context: click.Context, case_id: Optional[str], dry_run: bool, force: bool): +def nipt_upload_case(context: click.Context, case_id: str | None, dry_run: bool, force: bool): """Upload NIPT result files for a case""" LOG.info("*** NIPT UPLOAD START ***") diff --git a/cg/cli/upload/observations/observations.py b/cg/cli/upload/observations/observations.py index fcc1c2a504..ef6121c530 100644 --- a/cg/cli/upload/observations/observations.py +++ b/cg/cli/upload/observations/observations.py @@ -3,7 +3,6 @@ import contextlib import logging from datetime import datetime -from typing import Optional, Union import click from pydantic.v1 import ValidationError @@ -33,16 +32,16 @@ @ARGUMENT_CASE_ID @OPTION_DRY @click.pass_obj -def upload_observations_to_loqusdb(context: CGConfig, case_id: Optional[str], dry_run: bool): +def upload_observations_to_loqusdb(context: CGConfig, case_id: str | None, dry_run: bool): """Upload observations from an analysis to Loqusdb.""" click.echo(click.style("----------------- OBSERVATIONS -----------------")) with contextlib.suppress(LoqusdbError): case: Case = get_observations_case_to_upload(context, case_id) - observations_api: Union[ - MipDNAObservationsAPI, BalsamicObservationsAPI - ] = get_observations_api(context, case) + observations_api: MipDNAObservationsAPI | BalsamicObservationsAPI = get_observations_api( + context, case + ) if dry_run: LOG.info(f"Dry run. Would upload observations for {case.internal_id}.") @@ -56,7 +55,7 @@ def upload_observations_to_loqusdb(context: CGConfig, case_id: Optional[str], dr @OPTION_DRY @click.pass_context def upload_available_observations_to_loqusdb( - context: click.Context, pipeline: Optional[Pipeline], dry_run: bool + context: click.Context, pipeline: Pipeline | None, dry_run: bool ): """Uploads the available observations to Loqusdb.""" diff --git a/cg/cli/upload/observations/utils.py b/cg/cli/upload/observations/utils.py index 9c6d86018a..577fb3329e 100644 --- a/cg/cli/upload/observations/utils.py +++ b/cg/cli/upload/observations/utils.py @@ -1,7 +1,6 @@ """Helper functions for observations related actions.""" import logging -from typing import Union from sqlalchemy.orm import Query @@ -52,7 +51,7 @@ def get_observations_case_to_upload(context: CGConfig, case_id: str) -> Case: def get_observations_api( context: CGConfig, case: Case -) -> Union[MipDNAObservationsAPI, BalsamicObservationsAPI]: +) -> MipDNAObservationsAPI | BalsamicObservationsAPI: """Return an observations API given a specific case object.""" observations_apis = { Pipeline.MIP_DNA: MipDNAObservationsAPI(context, get_sequencing_method(case)), diff --git a/cg/cli/upload/scout.py b/cg/cli/upload/scout.py index 011a63550c..c99c2aba73 100644 --- a/cg/cli/upload/scout.py +++ b/cg/cli/upload/scout.py @@ -1,7 +1,6 @@ """Code for uploading to scout via CLI""" import logging from pathlib import Path -from typing import Optional import click from housekeeper.store.models import File, Version @@ -140,7 +139,7 @@ def upload_case_to_scout(context: CGConfig, re_upload: bool, dry_run: bool, case tag_name: str = UploadScoutAPI.get_load_config_tag() version: Version = housekeeper_api.last_version(bundle=case_id) - scout_config_file: Optional[File] = housekeeper_api.get_latest_file_from_version( + scout_config_file: File | None = housekeeper_api.get_latest_file_from_version( version=version, tags={tag_name} ) diff --git a/cg/cli/upload/utils.py b/cg/cli/upload/utils.py index f7dfd4cb93..b51a51b66f 100644 --- a/cg/cli/upload/utils.py +++ b/cg/cli/upload/utils.py @@ -1,7 +1,6 @@ """Utility functions for the upload cli commands.""" import logging -from typing import Optional import click @@ -13,7 +12,7 @@ LOG = logging.getLogger(__name__) -def suggest_cases_to_upload(status_db: Store, pipeline: Optional[Pipeline] = None) -> None: +def suggest_cases_to_upload(status_db: Store, pipeline: Pipeline | None = None) -> None: """Print a list of suggested cases to upload.""" LOG.warning("Provide a case, suggestions:") records: list[Analysis] = status_db.get_analyses_to_upload(pipeline=pipeline)[ diff --git a/cg/cli/upload/validate.py b/cg/cli/upload/validate.py index 82e2909a34..c6813a210f 100644 --- a/cg/cli/upload/validate.py +++ b/cg/cli/upload/validate.py @@ -1,5 +1,5 @@ """Code for validating an upload via CLI""" -from typing import Optional + import click @@ -13,7 +13,7 @@ @click.command() @click.argument("family_id", required=False) @click.pass_obj -def validate(context: CGConfig, family_id: Optional[str]): +def validate(context: CGConfig, family_id: str | None): """Validate a family of samples.""" status_db: Store = context.status_db diff --git a/cg/cli/workflow/commands.py b/cg/cli/workflow/commands.py index 4c038e4e5b..1134cb73dc 100644 --- a/cg/cli/workflow/commands.py +++ b/cg/cli/workflow/commands.py @@ -2,7 +2,6 @@ import logging import shutil from pathlib import Path -from typing import Union import click from dateutil.parser import parse as parse_date @@ -171,7 +170,7 @@ def clean_run_dir(context: CGConfig, yes: bool, case_id: str, dry_run: bool = Fa analysis_api: AnalysisAPI = context.meta_apis["analysis_api"] analysis_api.check_analysis_ongoing(case_id=case_id) - analysis_path: Union[list[Path], Path] = analysis_api.get_case_path(case_id) + analysis_path: list[Path] | Path = analysis_api.get_case_path(case_id) if dry_run: LOG.info(f"Would have deleted: {analysis_path}") diff --git a/cg/cli/workflow/microsalt/base.py b/cg/cli/workflow/microsalt/base.py index 484136dd60..b99f71f59d 100644 --- a/cg/cli/workflow/microsalt/base.py +++ b/cg/cli/workflow/microsalt/base.py @@ -2,7 +2,7 @@ import logging from pathlib import Path -from typing import Any, List +from typing import Any import click diff --git a/cg/cli/workflow/mip/base.py b/cg/cli/workflow/mip/base.py index 5ca098376e..9f1bd003cb 100644 --- a/cg/cli/workflow/mip/base.py +++ b/cg/cli/workflow/mip/base.py @@ -1,6 +1,5 @@ """Module for common workflow commands.""" import logging -from typing import Optional import click @@ -37,7 +36,7 @@ def config_case(context: CGConfig, case_id: str, panel_bed: str, dry_run: bool): analysis_api: MipAnalysisAPI = context.meta_apis["analysis_api"] try: analysis_api.status_db.verify_case_exists(case_internal_id=case_id) - panel_bed: Optional[str] = analysis_api.get_panel_bed(panel_bed=panel_bed) + panel_bed: str | None = analysis_api.get_panel_bed(panel_bed=panel_bed) config_data: dict = analysis_api.pedigree_config(case_id=case_id, panel_bed=panel_bed) except CgError as error: LOG.error(error) diff --git a/cg/cli/workflow/rnafusion/base.py b/cg/cli/workflow/rnafusion/base.py index 67b31e2c3b..5472e5b8cc 100644 --- a/cg/cli/workflow/rnafusion/base.py +++ b/cg/cli/workflow/rnafusion/base.py @@ -2,7 +2,6 @@ import logging from pathlib import Path -from typing import Optional import click from pydantic.v1 import ValidationError @@ -98,7 +97,7 @@ def run( revision: str, compute_env: str, use_nextflow: bool, - nf_tower_id: Optional[str], + nf_tower_id: str | None, dry_run: bool, ) -> None: """Run rnafusion analysis for given CASE ID.""" diff --git a/cg/cli/workflow/taxprofiler/base.py b/cg/cli/workflow/taxprofiler/base.py index 4f066dd45a..c9da3117cd 100644 --- a/cg/cli/workflow/taxprofiler/base.py +++ b/cg/cli/workflow/taxprofiler/base.py @@ -1,7 +1,6 @@ """CLI support to create config and/or start TAXPROFILER.""" import logging -from typing import Optional import click from pydantic.v1 import ValidationError @@ -95,7 +94,7 @@ def run( revision: str, compute_env: str, use_nextflow: bool, - nf_tower_id: Optional[str], + nf_tower_id: str | None, dry_run: bool, ) -> None: """Run taxprofiler analysis for a case.""" @@ -164,7 +163,7 @@ def start( revision: str, compute_env: str, use_nextflow: bool, - nf_tower_id: Optional[str], + nf_tower_id: str | None, dry_run: bool, ) -> None: """Start full workflow for case id.""" diff --git a/cg/constants/subject.py b/cg/constants/subject.py index 5e271d91d4..9e7ea8a63b 100644 --- a/cg/constants/subject.py +++ b/cg/constants/subject.py @@ -8,9 +8,6 @@ class Gender(StrEnum): OTHER = "other" MISSING = "" - def __repr__(self): - return self.value - class PhenotypeStatus(StrEnum): UNKNOWN = "unknown" diff --git a/cg/io/csv.py b/cg/io/csv.py index 931ef31fe6..9f829f93f2 100644 --- a/cg/io/csv.py +++ b/cg/io/csv.py @@ -2,7 +2,7 @@ import csv import io from pathlib import Path -from typing import Any, Union +from typing import Any from cg.constants import FileExtensions from cg.io.validate_path import validate_file_suffix @@ -12,7 +12,7 @@ def read_csv( file_path: Path, read_to_dict: bool = False, delimiter: str = "," -) -> Union[list[list[str]], list[dict]]: +) -> list[list[str]] | list[dict]: """ Read content in a CSV file to a list of list or list of dict. The delimiter parameter can be used to read TSV files. diff --git a/cg/io/txt.py b/cg/io/txt.py index f6d3c5e1a3..5b9b6bcb36 100644 --- a/cg/io/txt.py +++ b/cg/io/txt.py @@ -1,10 +1,9 @@ """Module to read or write txt files.""" from pathlib import Path -from typing import Union -def read_txt(file_path: Path, read_to_string: bool = False) -> Union[list[str], str]: +def read_txt(file_path: Path, read_to_string: bool = False) -> list[str] | str: """Read content in a TXT file.""" with open(file_path, "r") as file: if read_to_string: diff --git a/cg/meta/archive/archive.py b/cg/meta/archive/archive.py index 5fb8dad1c4..c1c6ab9b51 100644 --- a/cg/meta/archive/archive.py +++ b/cg/meta/archive/archive.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Callable, Optional, Type +from typing import Callable, Type from housekeeper.store.models import Archive, File from pydantic import BaseModel, ConfigDict @@ -169,11 +169,9 @@ def set_archive_retrieval_task_ids(self, retrieval_task_id: int, files: list[Fil file_id=file.id, retrieval_task_id=retrieval_task_id ) - def get_sample(self, file: File) -> Optional[Sample]: + def get_sample(self, file: File) -> Sample | None: """Fetches the Sample corresponding to a File and logs if a Sample is not found.""" - sample: Optional[Sample] = self.status_db.get_sample_by_internal_id( - file.version.bundle.name - ) + sample: Sample | None = self.status_db.get_sample_by_internal_id(file.version.bundle.name) if not sample: LOG.warning( f"No sample found in status_db corresponding to sample_id {file.version.bundle.name}." diff --git a/cg/meta/archive/ddn_dataflow.py b/cg/meta/archive/ddn_dataflow.py index fb48711d3b..f78ffb4602 100644 --- a/cg/meta/archive/ddn_dataflow.py +++ b/cg/meta/archive/ddn_dataflow.py @@ -3,7 +3,6 @@ from datetime import datetime from enum import StrEnum from pathlib import Path -from typing import Optional from urllib.parse import urljoin from housekeeper.store.models import File @@ -165,7 +164,7 @@ class AuthToken(BaseModel): access: str expire: int - refresh: Optional[str] = None + refresh: str | None = None class TransferJob(BaseModel): @@ -192,28 +191,28 @@ class SubJob(BaseModel): class GetJobStatusResponse(BaseModel): """Model representing the response fields from a get_job_status post.""" - request_date: Optional[datetime] = None - operation: Optional[str] = None + request_date: datetime | None = None + operation: str | None = None job_id: int - type: Optional[str] = None - status: Optional[int] = None + type: str | None = None + status: int | None = None description: str - start_date: Optional[datetime] = None - end_date: Optional[datetime] = None - durationTime: Optional[int] = None - priority: Optional[int] = None - progress: Optional[float] = None - subjobs: Optional[list[SubJob]] = None + start_date: datetime | None = None + end_date: datetime | None = None + durationTime: int | None = None + priority: int | None = None + progress: float | None = None + subjobs: list[SubJob] | None = None class GetJobStatusPayload(BaseModel): """Model representing the payload for a get_job_status request.""" job_id: int - subjob_id: Optional[int] = None - related_jobs: Optional[bool] = None - main_subjob: Optional[bool] = None - debug: Optional[bool] = None + subjob_id: int | None = None + related_jobs: bool | None = None + main_subjob: bool | None = None + debug: bool | None = None def post_request(self, url: str, headers: dict) -> GetJobStatusResponse: """Sends a request to the given url with the given headers, and its own content as diff --git a/cg/meta/backup/backup.py b/cg/meta/backup/backup.py index 4824bc3eae..d3e867188d 100644 --- a/cg/meta/backup/backup.py +++ b/cg/meta/backup/backup.py @@ -2,7 +2,6 @@ import logging import subprocess from pathlib import Path -from typing import Optional from housekeeper.store.models import File @@ -54,14 +53,14 @@ def check_processing(self) -> bool: LOG.debug(f"Processing flow cells: {processing_flow_cells_count}") return processing_flow_cells_count < MAX_PROCESSING_FLOW_CELLS - def get_first_flow_cell(self) -> Optional[Flowcell]: + def get_first_flow_cell(self) -> Flowcell | None: """Get the first flow cell from the requested queue.""" - flow_cell: Optional[Flowcell] = self.status.get_flow_cells_by_statuses( + flow_cell: Flowcell | None = self.status.get_flow_cells_by_statuses( flow_cell_statuses=[FlowCellStatus.REQUESTED] ) return flow_cell[0] if flow_cell else None - def fetch_flow_cell(self, flow_cell: Optional[Flowcell] = None) -> Optional[float]: + def fetch_flow_cell(self, flow_cell: Flowcell | None = None) -> float | None: """Start fetching a flow cell from backup if possible. 1. The processing queue is not full. @@ -72,7 +71,7 @@ def fetch_flow_cell(self, flow_cell: Optional[Flowcell] = None) -> Optional[floa return None if not flow_cell: - flow_cell: Optional[Flowcell] = self.get_first_flow_cell() + flow_cell: Flowcell | None = self.get_first_flow_cell() if not flow_cell: LOG.info("No flow cells requested") @@ -257,7 +256,7 @@ def retrieve_archived_file(self, archived_file: Path, run_dir: Path) -> None: ) @classmethod - def get_archived_flow_cell_path(cls, dsmc_output: list[str]) -> Optional[Path]: + def get_archived_flow_cell_path(cls, dsmc_output: list[str]) -> Path | None: """Get the path of the archived flow cell from a PDC query.""" flow_cell_line: str = [ row @@ -273,7 +272,7 @@ def get_archived_flow_cell_path(cls, dsmc_output: list[str]) -> Optional[Path]: return archived_flow_cell @classmethod - def get_archived_encryption_key_path(cls, dsmc_output: list[str]) -> Optional[Path]: + def get_archived_encryption_key_path(cls, dsmc_output: list[str]) -> Path | None: """Get the encryption key for the archived flow cell from a PDC query.""" encryption_key_line: str = [ row diff --git a/cg/meta/clean/api.py b/cg/meta/clean/api.py index 2971a12e5d..50700dbaa8 100644 --- a/cg/meta/clean/api.py +++ b/cg/meta/clean/api.py @@ -1,7 +1,7 @@ import logging from datetime import datetime from pathlib import Path -from typing import Iterator, Optional +from typing import Iterator from housekeeper.store.models import File, Version @@ -32,7 +32,7 @@ def get_bundle_files(self, before: datetime, pipeline: Pipeline) -> Iterator[lis ): bundle_name = analysis.case.internal_id - hk_bundle_version: Optional[Version] = self.housekeeper_api.version( + hk_bundle_version: Version | None = self.housekeeper_api.version( bundle=bundle_name, date=analysis.started_at ) if not hk_bundle_version: diff --git a/cg/meta/clean/clean_flow_cells.py b/cg/meta/clean/clean_flow_cells.py index 4b7d62f9fe..0a6cc7d1b4 100644 --- a/cg/meta/clean/clean_flow_cells.py +++ b/cg/meta/clean/clean_flow_cells.py @@ -1,7 +1,6 @@ """An API that handles the cleaning of flow cells.""" import logging from pathlib import Path -from typing import Optional from housekeeper.store.models import File @@ -135,7 +134,7 @@ def has_sample_fastq_or_spring_files_in_housekeeper(self) -> bool: ) return True - def get_flow_cell_from_status_db(self) -> Optional[Flowcell]: + def get_flow_cell_from_status_db(self) -> Flowcell | None: """ Get the flow cell entry from StatusDB. Raises: @@ -146,7 +145,7 @@ def get_flow_cell_from_status_db(self) -> Optional[Flowcell]: raise ValueError(f"Flow cell {self.flow_cell.id} not found in StatusDB.") return flow_cell - def get_sequencing_metrics_for_flow_cell(self) -> Optional[list[SampleLaneSequencingMetrics]]: + def get_sequencing_metrics_for_flow_cell(self) -> list[SampleLaneSequencingMetrics] | None: """ Get the SampleLaneSequencingMetrics entries for a flow cell. Raises: diff --git a/cg/meta/compress/files.py b/cg/meta/compress/files.py index d3c2a2c25e..36bf67379b 100644 --- a/cg/meta/compress/files.py +++ b/cg/meta/compress/files.py @@ -3,7 +3,6 @@ import datetime import logging from pathlib import Path -from typing import Optional from housekeeper.store.models import File, Version @@ -80,7 +79,7 @@ def get_compression_data(fastq_files: list[Path]) -> list[CompressionData]: compressions: list[CompressionData] = [] for fastq_file in fastq_files: # file prefix is the run name identifier - file_prefix: Optional[Path] = get_fastq_stub(fastq_file) + file_prefix: Path | None = get_fastq_stub(fastq_file) if file_prefix is None: LOG.info(f"Invalid FASTQ name {fastq_file}") continue diff --git a/cg/meta/demultiplex/delete_demultiplex_api.py b/cg/meta/demultiplex/delete_demultiplex_api.py index a6cdb015ac..24df1a953a 100644 --- a/cg/meta/demultiplex/delete_demultiplex_api.py +++ b/cg/meta/demultiplex/delete_demultiplex_api.py @@ -3,7 +3,7 @@ import shutil from glob import glob from pathlib import Path -from typing import Iterable, Optional +from typing import Iterable from housekeeper.store.models import File @@ -66,7 +66,7 @@ def _set_samples_on_flow_cell(self) -> None: flow_cell = self.status_db.get_flow_cell_by_name(flow_cell_name=self.flow_cell_name) self.samples_on_flow_cell: list[Sample] = flow_cell.samples - def active_samples_on_flow_cell(self) -> Optional[list[str]]: + def active_samples_on_flow_cell(self) -> list[str] | None: """Check if there are any active cases related to samples of a flow cell.""" active_samples_on_flow_cell: list[str] = [ sample.internal_id diff --git a/cg/meta/demultiplex/demux_post_processing.py b/cg/meta/demultiplex/demux_post_processing.py index 23b46b9676..6d3baa14e5 100644 --- a/cg/meta/demultiplex/demux_post_processing.py +++ b/cg/meta/demultiplex/demux_post_processing.py @@ -1,7 +1,6 @@ """Post-processing Demultiplex API.""" import logging from pathlib import Path -from typing import Optional from cg.apps.housekeeper.hk import HousekeeperAPI from cg.exc import FlowCellError, MissingFilesError @@ -10,8 +9,8 @@ ) from cg.meta.demultiplex.status_db_storage_functions import ( store_flow_cell_data_in_status_db, - store_sequencing_metrics_in_status_db, store_sample_data_in_status_db, + store_sequencing_metrics_in_status_db, ) from cg.meta.demultiplex.utils import create_delivery_file_in_flow_cell_directory from cg.meta.demultiplex.validation import is_flow_cell_ready_for_postprocessing @@ -40,7 +39,7 @@ def set_dry_run(self, dry_run: bool) -> None: def finish_flow_cell( self, flow_cell_directory_name: str, - bcl_converter: Optional[str] = None, + bcl_converter: str | None = None, force: bool = False, ) -> None: """Store data for the demultiplexed flow cell and mark it as ready for delivery. diff --git a/cg/meta/demultiplex/housekeeper_storage_functions.py b/cg/meta/demultiplex/housekeeper_storage_functions.py index a77b4076cb..5e32efd256 100644 --- a/cg/meta/demultiplex/housekeeper_storage_functions.py +++ b/cg/meta/demultiplex/housekeeper_storage_functions.py @@ -1,7 +1,6 @@ """Functions interacting with housekeeper in the DemuxPostProcessingAPI.""" import logging from pathlib import Path -from typing import Optional from cg.apps.housekeeper.hk import HousekeeperAPI from cg.constants.housekeeper_tags import SequencingFileTag @@ -96,7 +95,7 @@ def add_sample_fastq_files_to_housekeeper( sample_internal_ids: list[str] = flow_cell.sample_sheet.get_sample_ids() for sample_internal_id in sample_internal_ids: - sample_fastq_paths: Optional[list[Path]] = get_sample_fastqs_from_flow_cell( + sample_fastq_paths: list[Path] | None = get_sample_fastqs_from_flow_cell( flow_cell_directory=flow_cell.path, sample_internal_id=sample_internal_id ) diff --git a/cg/meta/demultiplex/status_db_storage_functions.py b/cg/meta/demultiplex/status_db_storage_functions.py index 229c818222..85581d9362 100644 --- a/cg/meta/demultiplex/status_db_storage_functions.py +++ b/cg/meta/demultiplex/status_db_storage_functions.py @@ -1,7 +1,6 @@ """Functions interacting with statusdb in the DemuxPostProcessingAPI.""" import datetime import logging -from typing import Optional from cg.apps.sequencing_metrics_parser.api import ( create_sample_lane_sequencing_metrics_for_flow_cell, @@ -27,7 +26,7 @@ def store_flow_cell_data_in_status_db( Create flow cell from the parsed and validated flow cell data. And add the samples on the flow cell to the model. """ - flow_cell: Optional[Flowcell] = store.get_flow_cell_by_name(flow_cell_name=parsed_flow_cell.id) + flow_cell: Flowcell | None = store.get_flow_cell_by_name(flow_cell_name=parsed_flow_cell.id) if not flow_cell: flow_cell: Flowcell = Flowcell( name=parsed_flow_cell.id, @@ -111,12 +110,12 @@ def metric_has_sample_in_statusdb(sample_internal_id: str, store: Store) -> bool def metric_exists_in_status_db(metric: SampleLaneSequencingMetrics, store: Store) -> bool: - existing_metrics_entry: Optional[ - SampleLaneSequencingMetrics - ] = store.get_metrics_entry_by_flow_cell_name_sample_internal_id_and_lane( - flow_cell_name=metric.flow_cell_name, - sample_internal_id=metric.sample_internal_id, - lane=metric.flow_cell_lane_number, + existing_metrics_entry: SampleLaneSequencingMetrics | None = ( + store.get_metrics_entry_by_flow_cell_name_sample_internal_id_and_lane( + flow_cell_name=metric.flow_cell_name, + sample_internal_id=metric.sample_internal_id, + lane=metric.flow_cell_lane_number, + ) ) if existing_metrics_entry: LOG.warning( @@ -132,7 +131,7 @@ def store_sample_data_in_status_db(flow_cell: FlowCellDirectoryData, store: Stor sequenced_at: datetime = flow_cell.sequenced_at for sample_id in sample_internal_ids: - sample: Optional[Sample] = store.get_sample_by_internal_id(sample_id) + sample: Sample | None = store.get_sample_by_internal_id(sample_id) if not sample: LOG.warning(f"Cannot find {sample_id}. Skipping.") diff --git a/cg/meta/demultiplex/utils.py b/cg/meta/demultiplex/utils.py index f2a3b0c5a3..3b8951fd62 100644 --- a/cg/meta/demultiplex/utils.py +++ b/cg/meta/demultiplex/utils.py @@ -2,10 +2,11 @@ import os import re from pathlib import Path -from typing import Optional from cg.apps.demultiplex.sample_sheet.models import SampleSheet -from cg.apps.demultiplex.sample_sheet.read_sample_sheet import get_sample_sheet_from_file +from cg.apps.demultiplex.sample_sheet.read_sample_sheet import ( + get_sample_sheet_from_file, +) from cg.constants.constants import FileExtensions from cg.constants.demultiplexing import DemultiplexingDirsAndFiles from cg.constants.sequencing import FLOWCELL_Q30_THRESHOLD, Sequencers @@ -90,7 +91,7 @@ def get_valid_sample_fastqs(fastq_paths: list[Path], sample_internal_id: str) -> def get_sample_fastqs_from_flow_cell( flow_cell_directory: Path, sample_internal_id: str -) -> Optional[list[Path]]: +) -> list[Path] | None: """Retrieve all fastq files for a specific sample in a flow cell directory.""" # The flat output structure for NovaseqX flow cells demultiplexed with BCLConvert on hasta diff --git a/cg/meta/demultiplex/validation.py b/cg/meta/demultiplex/validation.py index 86dfd51654..067f4e0a9b 100644 --- a/cg/meta/demultiplex/validation.py +++ b/cg/meta/demultiplex/validation.py @@ -1,6 +1,5 @@ import logging from pathlib import Path -from typing import Optional from cg.constants.demultiplexing import DemultiplexingDirsAndFiles from cg.exc import FlowCellError, MissingFilesError @@ -47,7 +46,7 @@ def validate_flow_cell_has_fastq_files(flow_cell: FlowCellDirectoryData) -> None """ sample_ids: list[str] = flow_cell.sample_sheet.get_sample_ids() for sample_id in sample_ids: - fastq_files: Optional[list[Path]] = get_sample_fastqs_from_flow_cell( + fastq_files: list[Path] | None = get_sample_fastqs_from_flow_cell( flow_cell_directory=flow_cell.path, sample_internal_id=sample_id ) if fastq_files: diff --git a/cg/meta/encryption/encryption.py b/cg/meta/encryption/encryption.py index 853521f951..ee97a07356 100644 --- a/cg/meta/encryption/encryption.py +++ b/cg/meta/encryption/encryption.py @@ -4,7 +4,6 @@ from io import TextIOWrapper from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Optional, Union from cg.apps.slurm.slurm_api import SlurmAPI from cg.constants import SPACE, FileExtensions @@ -148,7 +147,7 @@ def __init__( encryption_dir: Path, flow_cell: FlowCellDirectoryData, pigz_binary_path: str, - sbatch_parameter: dict[str, Union[str, int]], + sbatch_parameter: dict[str, str | int], slurm_api: SlurmAPI, tar_api: TarAPI, dry_run: bool = False, @@ -240,7 +239,7 @@ def get_flow_cell_symmetric_decryption_command( ) return SPACE.join(decryption_parameters) - def is_encryption_possible(self) -> Optional[bool]: + def is_encryption_possible(self) -> bool | None: """Check if requirements for encryption are meet. Raises: FlowCellError if sequencing is not ready, encryption is pending or complete. diff --git a/cg/meta/invoice.py b/cg/meta/invoice.py index 64c83ee2f8..05f020f9da 100644 --- a/cg/meta/invoice.py +++ b/cg/meta/invoice.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Union +from typing import Any from pydantic.v1 import ValidationError @@ -29,7 +29,7 @@ def __init__(self, db: Store, lims_api: LimsAPI, invoice_obj: Invoice): self.raw_records = [] self._set_record_type() self.genologics_lims: FlaskLims = genologics_lims - self.invoice_info: Optional[InvoiceInfo] = None + self.invoice_info: InvoiceInfo | None = None def _set_record_type(self): """Define the record_type based on the invoice object. @@ -41,7 +41,7 @@ def _set_record_type(self): self.record_type = RecordType.Sample self.raw_records = self.invoice_obj.samples - def get_customer_by_cost_center(self, cost_center: str) -> Union[Customer, str]: + def get_customer_by_cost_center(self, cost_center: str) -> Customer | str: """Return the costumer based on cost center.""" return ( self.db.get_customer_by_internal_id(customer_internal_id=CustomerNames.cust999) @@ -57,8 +57,8 @@ def get_customer_invoice_contact(self, customer: Customer, msg: str) -> Any: return customer.invoice_contact def get_contact( - self, customer: Customer, customer_invoice_contact: Optional[User], msg: str - ) -> Optional[InvoiceContact]: + self, customer: Customer, customer_invoice_contact: User | None, msg: str + ) -> InvoiceContact | None: """Return the contact information.""" try: contact = InvoiceContact( @@ -74,7 +74,7 @@ def get_contact( self.log.append(msg) return None - def get_contact_info(self, cost_center: str) -> Optional[InvoiceContact]: + def get_contact_info(self, cost_center: str) -> InvoiceContact | None: """Return contact info for a customer.""" msg = ( @@ -87,7 +87,7 @@ def get_contact_info(self, cost_center: str) -> Optional[InvoiceContact]: customer_invoice_contact=customer_invoice_contact, customer=customer, msg=msg ) - def get_invoice_report(self, cost_center: str) -> Optional[dict]: + def get_invoice_report(self, cost_center: str) -> dict | None: """Return invoice information as dictionary to generate Excel report.""" records: list[dict] = [] @@ -131,7 +131,7 @@ def get_invoice_report(self, cost_center: str) -> Optional[dict]: self.log.append("ValidationError in InvoiceReport class.") return None - def _discount_price(self, record: Sample or Pool, discount: int = 0) -> Optional[int]: + def _discount_price(self, record: Sample or Pool, discount: int = 0) -> int | None: """Return discount price for a sample or pool.""" priority = self.get_priority(record, for_discount_price=True) full_price = getattr(record.application_version, f"price_{priority}") @@ -142,7 +142,7 @@ def _discount_price(self, record: Sample or Pool, discount: int = 0) -> Optional def _cost_center_split_factor( self, price: int, cost_center: str, percent_kth: int, tag: str, version: str - ) -> Optional[int]: + ) -> int | None: """Return split price based on cost center.""" if price: try: @@ -186,9 +186,7 @@ def get_invoice_entity_record( return invoice_info - def get_application( - self, record: Sample or Pool, discount: int - ) -> Optional[InvoiceApplication]: + def get_application(self, record: Sample or Pool, discount: int) -> InvoiceApplication | None: """Return the application information.""" try: application = InvoiceApplication( @@ -265,7 +263,7 @@ def set_invoice_info(self, invoice_info: InvoiceInfo): """Set invoice_info.""" self.invoice_info = invoice_info - def total_price(self) -> Optional[float]: + def total_price(self) -> float | None: """Return the total price for all records in the invoice.""" discount = self.invoice_obj.discount diff --git a/cg/meta/observations/observations_api.py b/cg/meta/observations/observations_api.py index d232595ba5..80f1f0ada4 100644 --- a/cg/meta/observations/observations_api.py +++ b/cg/meta/observations/observations_api.py @@ -3,7 +3,6 @@ import logging from datetime import datetime from pathlib import Path -from typing import Optional, Union from housekeeper.store.models import Version @@ -21,7 +20,7 @@ MipDNAObservationsInputFiles, ) from cg.store import Store -from cg.store.models import Analysis, Customer, Case +from cg.store.models import Analysis, Case, Customer LOG = logging.getLogger(__name__) @@ -40,14 +39,14 @@ def __init__(self, config: CGConfig): def upload(self, case: Case) -> None: """Upload observations to Loqusdb.""" self.check_customer_loqusdb_permissions(case.customer) - input_files: Union[ - MipDNAObservationsInputFiles, BalsamicObservationsInputFiles - ] = self.get_observations_input_files(case) + input_files: MipDNAObservationsInputFiles | BalsamicObservationsInputFiles = ( + self.get_observations_input_files(case) + ) self.load_observations(case=case, input_files=input_files) def get_observations_input_files( self, case: Case - ) -> Union[MipDNAObservationsInputFiles, BalsamicObservationsInputFiles]: + ) -> MipDNAObservationsInputFiles | BalsamicObservationsInputFiles: """Fetch input files from a case to upload to Loqusdb.""" analysis: Analysis = case.analyses[0] analysis_date: datetime = analysis.started_at or analysis.completed_at @@ -80,8 +79,8 @@ def get_loqusdb_api(self, loqusdb_instance: LoqusdbInstance) -> LoqusdbAPI: def is_duplicate( case: Case, loqusdb_api: LoqusdbAPI, - profile_vcf_path: Optional[Path], - profile_threshold: Optional[float], + profile_vcf_path: Path | None, + profile_threshold: float | None, ) -> bool: """Check if a case has already been uploaded to Loqusdb.""" loqusdb_case: dict = loqusdb_api.get_case(case_id=case.internal_id) @@ -94,7 +93,7 @@ def is_duplicate( ) return bool(loqusdb_case or duplicate or case.loqusdb_uploaded_samples) - def update_statusdb_loqusdb_id(self, samples: list[Case], loqusdb_id: Optional[str]) -> None: + def update_statusdb_loqusdb_id(self, samples: list[Case], loqusdb_id: str | None) -> None: """Update Loqusdb ID field in StatusDB for each of the provided samples.""" for sample in samples: sample.loqusdb_id = loqusdb_id @@ -109,21 +108,21 @@ def check_customer_loqusdb_permissions(self, customer: Customer) -> None: raise LoqusdbUploadCaseError LOG.info(f"Valid customer {customer.internal_id} for Loqusdb uploads") - def get_loqusdb_customers(self) -> Union[LoqusdbMipCustomers, LoqusdbBalsamicCustomers]: + def get_loqusdb_customers(self) -> LoqusdbMipCustomers | LoqusdbBalsamicCustomers: """Returns the customers that are entitled to Loqusdb uploads.""" raise NotImplementedError def load_observations( self, case: Case, - input_files: Union[MipDNAObservationsInputFiles, BalsamicObservationsInputFiles], + input_files: MipDNAObservationsInputFiles | BalsamicObservationsInputFiles, ) -> None: """Load observation counts to Loqusdb.""" raise NotImplementedError def extract_observations_files_from_hk( self, hk_version: Version - ) -> Union[MipDNAObservationsInputFiles, BalsamicObservationsInputFiles]: + ) -> MipDNAObservationsInputFiles | BalsamicObservationsInputFiles: """Extract observations files given a housekeeper version.""" raise NotImplementedError diff --git a/cg/meta/orders/api.py b/cg/meta/orders/api.py index 55759955f3..927421bae2 100644 --- a/cg/meta/orders/api.py +++ b/cg/meta/orders/api.py @@ -7,7 +7,6 @@ be validated and if passing all checks be accepted as new samples. """ import logging -from typing import Optional from cg.apps.lims import LimsAPI from cg.apps.osticket import OsTicket @@ -70,7 +69,7 @@ def submit(self, project: OrderType, order_in: OrderIn, user_name: str, user_mai submit_handler.validate_order(order=order_in) # detect manual ticket assignment - ticket_number: Optional[str] = TicketHandler.parse_ticket_number(order_in.name) + ticket_number: str | None = TicketHandler.parse_ticket_number(order_in.name) if not ticket_number: ticket_number = self.ticket_handler.create_ticket( order=order_in, user_name=user_name, user_mail=user_mail, project=project diff --git a/cg/meta/orders/ticket_handler.py b/cg/meta/orders/ticket_handler.py index de7f6504cf..2003f4d618 100644 --- a/cg/meta/orders/ticket_handler.py +++ b/cg/meta/orders/ticket_handler.py @@ -2,7 +2,7 @@ import re from pathlib import Path from tempfile import TemporaryDirectory -from typing import Any, Optional +from typing import Any from sendmail_container import FormDataRequest @@ -25,7 +25,7 @@ def __init__(self, osticket_api: OsTicket, status_db: Store): self.status_db: Store = status_db @staticmethod - def parse_ticket_number(name: str) -> Optional[str]: + def parse_ticket_number(name: str) -> str | None: """Try to parse a ticket number from a string""" # detect manual ticket assignment ticket_match = re.fullmatch(r"#([0-9]{6})", name) @@ -38,7 +38,7 @@ def parse_ticket_number(name: str) -> Optional[str]: def create_ticket( self, order: OrderIn, user_name: str, user_mail: str, project: str - ) -> Optional[int]: + ) -> int | None: """Create a ticket and return the ticket number""" message: str = self.create_new_ticket_header( message=self.create_xml_sample_list(order=order, user_name=user_name), @@ -46,7 +46,7 @@ def create_ticket( project=project, ) attachment: dict = self.create_attachment(order=order) - ticket_nr: Optional[str] = self.osticket.open_ticket( + ticket_nr: str | None = self.osticket.open_ticket( name=user_name, email=user_mail, subject=order.name, @@ -105,19 +105,19 @@ def add_sample_name_to_message(self, message: str, sample_name: str) -> str: return message @staticmethod - def add_sample_apptag_to_message(message: str, application: Optional[str]) -> str: + def add_sample_apptag_to_message(message: str, application: str | None) -> str: if application: message += f", application: {application}" return message @staticmethod - def add_sample_case_name_to_message(message: str, case_name: Optional[str]) -> str: + def add_sample_case_name_to_message(message: str, case_name: str | None) -> str: if case_name: message += f", case: {case_name}" return message def add_existing_sample_info_to_message( - self, message: str, customer_id: str, internal_id: Optional[str] + self, message: str, customer_id: str, internal_id: str | None ) -> str: if not internal_id: return message @@ -132,23 +132,23 @@ def add_existing_sample_info_to_message( return message @staticmethod - def add_sample_priority_to_message(message: str, priority: Optional[str]) -> str: + def add_sample_priority_to_message(message: str, priority: str | None) -> str: if priority: message += f", priority: {priority}" return message @staticmethod - def add_sample_comment_to_message(message: str, comment: Optional[str]) -> str: + def add_sample_comment_to_message(message: str, comment: str | None) -> str: if comment: message += f", {comment}" return message - def add_order_comment_to_message(self, message: str, comment: Optional[str]) -> str: + def add_order_comment_to_message(self, message: str, comment: str | None) -> str: if comment: message += f"{self.NEW_LINE}{comment}." return message - def add_user_name_to_message(self, message: str, name: Optional[str]) -> str: + def add_user_name_to_message(self, message: str, name: str | None) -> str: if name: message += f"{self.NEW_LINE}{name}" return message diff --git a/cg/meta/report/balsamic.py b/cg/meta/report/balsamic.py index 05911ac445..f563efb723 100644 --- a/cg/meta/report/balsamic.py +++ b/cg/meta/report/balsamic.py @@ -1,5 +1,4 @@ import logging -from typing import Optional, Union from housekeeper.store.models import File, Version @@ -10,6 +9,7 @@ REQUIRED_CASE_FIELDS, REQUIRED_CUSTOMER_FIELDS, REQUIRED_DATA_ANALYSIS_BALSAMIC_FIELDS, + REQUIRED_DATA_ANALYSIS_FIELDS, REQUIRED_REPORT_FIELDS, REQUIRED_SAMPLE_BALSAMIC_FIELDS, REQUIRED_SAMPLE_METADATA_BALSAMIC_TARGETED_FIELDS, @@ -18,7 +18,6 @@ REQUIRED_SAMPLE_METHODS_FIELDS, REQUIRED_SAMPLE_TIMESTAMP_FIELDS, Pipeline, - REQUIRED_DATA_ANALYSIS_FIELDS, ) from cg.constants.scout_upload import BALSAMIC_CASE_TAGS from cg.meta.report.field_validators import get_million_read_pairs @@ -52,7 +51,7 @@ def __init__(self, config: CGConfig, analysis_api: BalsamicAnalysisAPI): def get_sample_metadata( self, case: Case, sample: Sample, analysis_metadata: BalsamicAnalysis - ) -> Union[BalsamicTargetedSampleMetadataModel, BalsamicWGSSampleMetadataModel]: + ) -> BalsamicTargetedSampleMetadataModel | BalsamicWGSSampleMetadataModel: """Return sample metadata to include in the report.""" sample_metrics: dict[str, BalsamicQCMetrics] = analysis_metadata.sample_metrics[ sample.internal_id @@ -120,7 +119,7 @@ def get_wgs_percent_duplication(sample_metrics: BalsamicWGSQCMetrics): else None ) - def get_data_analysis_type(self, case: Case) -> Optional[str]: + def get_data_analysis_type(self, case: Case) -> str | None: """Return data analysis type carried out.""" return self.analysis_api.get_bundle_deliverables_type(case_id=case.internal_id) @@ -152,9 +151,7 @@ def get_variant_callers(self, _analysis_metadata: BalsamicAnalysis) -> list: return analysis_var_callers @staticmethod - def get_variant_caller_version( - var_caller_name: str, var_caller_versions: dict - ) -> Optional[str]: + def get_variant_caller_version(var_caller_name: str, var_caller_versions: dict) -> str | None: """Return version of a specific Balsamic tool.""" for tool_name, versions in var_caller_versions.items(): if tool_name in var_caller_name: @@ -230,7 +227,7 @@ def get_upload_case_tags(self) -> dict: """Return Balsamic upload case tags.""" return BALSAMIC_CASE_TAGS - def get_scout_uploaded_file_from_hk(self, case_id: str, scout_tag: str) -> Optional[str]: + def get_scout_uploaded_file_from_hk(self, case_id: str, scout_tag: str) -> str | None: """Return file path of the uploaded to Scout file given its tag.""" version: Version = self.housekeeper_api.last_version(bundle=case_id) tags: list = self.get_hk_scout_file_tags(scout_tag=scout_tag) diff --git a/cg/meta/report/field_validators.py b/cg/meta/report/field_validators.py index 1899c2c19f..793a332c49 100644 --- a/cg/meta/report/field_validators.py +++ b/cg/meta/report/field_validators.py @@ -1,13 +1,12 @@ """Report field validation helper""" -from typing import Optional from cg.constants import NA_FIELD from cg.constants.constants import SCALE_TO_MILLION_READ_PAIRS from cg.models.report.report import ReportModel -def get_million_read_pairs(reads: int) -> Optional[float]: +def get_million_read_pairs(reads: int) -> float | None: """Return number of sequencing reads as millions of read pairs.""" return ( round(reads / SCALE_TO_MILLION_READ_PAIRS, 1) if reads or isinstance(reads, int) else None diff --git a/cg/meta/report/mip_dna.py b/cg/meta/report/mip_dna.py index 9011f9af5f..28c7c984db 100644 --- a/cg/meta/report/mip_dna.py +++ b/cg/meta/report/mip_dna.py @@ -1,5 +1,5 @@ import logging -from typing import Iterable, Optional +from typing import Iterable from housekeeper.store.models import File, Version @@ -131,7 +131,7 @@ def get_upload_case_tags(self) -> dict: """Return MIP DNA upload case tags.""" return MIP_CASE_TAGS - def get_scout_uploaded_file_from_hk(self, case_id: str, scout_tag: str) -> Optional[str]: + def get_scout_uploaded_file_from_hk(self, case_id: str, scout_tag: str) -> str | None: """Return file path of the uploaded to Scout file given its tag.""" version: Version = self.housekeeper_api.last_version(bundle=case_id) tags: list = self.get_hk_scout_file_tags(scout_tag=scout_tag) diff --git a/cg/meta/report/report_api.py b/cg/meta/report/report_api.py index 5f0152ac21..c2dd7a7d43 100644 --- a/cg/meta/report/report_api.py +++ b/cg/meta/report/report_api.py @@ -2,7 +2,6 @@ import logging from datetime import datetime from pathlib import Path -from typing import Optional import requests from housekeeper.store.models import File, Version @@ -83,7 +82,7 @@ def create_delivery_report_file( def add_delivery_report_to_hk( self, case_id: str, delivery_report_file: Path, version: Version - ) -> Optional[File]: + ) -> File | None: """Add a delivery report file to a case bundle and return its file object.""" LOG.info(f"Adding a new delivery report to housekeeper for {case_id}") file: File = self.housekeeper_api.add_file( @@ -93,7 +92,7 @@ def add_delivery_report_to_hk( self.housekeeper_api.add_commit(file) return file - def get_delivery_report_from_hk(self, case_id: str, version: Version) -> Optional[str]: + def get_delivery_report_from_hk(self, case_id: str, version: Version) -> str | None: """Return path of a delivery report stored in HK.""" delivery_report: File = self.housekeeper_api.get_latest_file( bundle=case_id, tags=[HK_DELIVERY_REPORT_TAG], version=version.id @@ -103,7 +102,7 @@ def get_delivery_report_from_hk(self, case_id: str, version: Version) -> Optiona return None return delivery_report.full_path - def get_scout_uploaded_file_from_hk(self, case_id: str, scout_tag: str) -> Optional[str]: + def get_scout_uploaded_file_from_hk(self, case_id: str, scout_tag: str) -> str | None: """Return the file path of the uploaded to Scout file given its tag.""" raise NotImplementedError @@ -241,7 +240,7 @@ def get_samples_data(self, case: Case, analysis_metadata: AnalysisModel) -> list ) for case_sample in case_samples: sample: Sample = case_sample.sample - lims_sample: Optional[dict] = self.get_lims_sample(sample_id=sample.internal_id) + lims_sample: dict | None = self.get_lims_sample(sample_id=sample.internal_id) samples.append( SampleModel( name=sample.name, @@ -261,7 +260,7 @@ def get_samples_data(self, case: Case, analysis_metadata: AnalysisModel) -> list ) return samples - def get_lims_sample(self, sample_id: str) -> Optional[dict]: + def get_lims_sample(self, sample_id: str) -> dict | None: """Fetches sample data from LIMS. Returns an empty dictionary if the request was unsuccessful.""" lims_sample = dict() try: @@ -376,12 +375,12 @@ def get_sample_metadata( """Return sample metadata to include in the report.""" raise NotImplementedError - def get_data_analysis_type(self, case: Case) -> Optional[str]: + def get_data_analysis_type(self, case: Case) -> str | None: """Return data analysis type carried out.""" case_sample: Sample = self.status_db.get_case_samples_by_case_id( case_internal_id=case.internal_id )[0].sample - lims_sample: Optional[dict] = self.get_lims_sample(sample_id=case_sample.internal_id) + lims_sample: dict | None = self.get_lims_sample(sample_id=case_sample.internal_id) application: Application = self.status_db.get_application_by_tag( tag=lims_sample.get("application") ) @@ -434,7 +433,7 @@ def get_timestamp_required_fields(case: CaseModel, required_fields: list) -> dic break return ReportAPI.get_sample_required_fields(case=case, required_fields=required_fields) - def get_hk_scout_file_tags(self, scout_tag: str) -> Optional[list]: + def get_hk_scout_file_tags(self, scout_tag: str) -> list | None: """Return pipeline specific uploaded to Scout Housekeeper file tags given a Scout key.""" tags = self.get_upload_case_tags().get(scout_tag) return list(tags) if tags else None diff --git a/cg/meta/report/rnafusion.py b/cg/meta/report/rnafusion.py index 67d11716e0..6a5772c1c0 100644 --- a/cg/meta/report/rnafusion.py +++ b/cg/meta/report/rnafusion.py @@ -1,5 +1,5 @@ """RNAfusion delivery report API.""" -from typing import Optional + from cg.constants import ( REQUIRED_APPLICATION_FIELDS, @@ -72,7 +72,7 @@ def get_report_accreditation( """Checks if the report is accredited or not. Rnafusion is not an accredited workflow.""" return False - def get_scout_uploaded_file_from_hk(self, case_id: str, scout_tag: str) -> Optional[str]: + def get_scout_uploaded_file_from_hk(self, case_id: str, scout_tag: str) -> str | None: """Return file path of the uploaded to Scout file given its tag.""" return None diff --git a/cg/meta/transfer/external_data.py b/cg/meta/transfer/external_data.py index 035f33094f..ddb1746955 100644 --- a/cg/meta/transfer/external_data.py +++ b/cg/meta/transfer/external_data.py @@ -1,7 +1,6 @@ import datetime as dt import logging from pathlib import Path -from typing import Optional from housekeeper.store.models import Version @@ -45,12 +44,12 @@ def get_source_path( self, customer: str, ticket: str, - cust_sample_id: Optional[str] = "", + cust_sample_id: str | None = "", ) -> Path: """Returns the path to where the sample files are fetched from""" return Path(self.source_path % customer, ticket, cust_sample_id) - def get_destination_path(self, customer: str, lims_sample_id: Optional[str] = "") -> Path: + def get_destination_path(self, customer: str, lims_sample_id: str | None = "") -> Path: """Returns the path to where the files are to be transferred""" return Path(self.destination_path % customer, lims_sample_id) @@ -105,7 +104,7 @@ def get_all_paths(self, customer: str, lims_sample_id: str) -> list[Path]: all_fastq_in_folder: list[Path] = self.get_all_fastq(sample_folder=fastq_folder) return all_fastq_in_folder - def check_fastq_md5sum(self, fastq_path) -> Optional[Path]: + def check_fastq_md5sum(self, fastq_path) -> Path | None: """Returns the path of the input file if it does not match its md5sum""" if Path(str(fastq_path) + ".md5").exists(): given_md5sum: str = extract_md5sum(md5sum_file=Path(str(fastq_path) + ".md5")) @@ -133,7 +132,7 @@ def add_files_to_bundles( def get_failed_fastq_paths(self, fastq_paths_to_add: list[Path]) -> list[Path]: failed_sum_paths: list[Path] = [] for path in fastq_paths_to_add: - failed_path: Optional[Path] = self.check_fastq_md5sum(path) + failed_path: Path | None = self.check_fastq_md5sum(path) if failed_path: failed_sum_paths.append(failed_path) return failed_sum_paths diff --git a/cg/meta/transfer/lims.py b/cg/meta/transfer/lims.py index a3b59256e7..32359737cf 100644 --- a/cg/meta/transfer/lims.py +++ b/cg/meta/transfer/lims.py @@ -1,6 +1,5 @@ import logging from enum import Enum -from typing import Union import genologics.entities @@ -105,7 +104,7 @@ def transfer_pools(self, status_type: PoolState): if not self._is_pool_valid(pool_obj, ticket, number_of_samples): continue - samples_in_pool: Union[dict[str:str], list[genologics.Sample]] = self.lims.get_samples( + samples_in_pool: dict[str, str] | list[genologics.Sample] = self.lims.get_samples( projectname=ticket ) for sample_obj in samples_in_pool: diff --git a/cg/meta/upload/fohm/fohm.py b/cg/meta/upload/fohm/fohm.py index e9221bb269..fc7bf166c2 100644 --- a/cg/meta/upload/fohm/fohm.py +++ b/cg/meta/upload/fohm/fohm.py @@ -4,7 +4,6 @@ import os import shutil from pathlib import Path -from typing import Optional import pandas as pd import paramiko @@ -24,7 +23,7 @@ class FOHMUploadAPI: - def __init__(self, config: CGConfig, dry_run: bool = False, datestr: Optional[str] = None): + def __init__(self, config: CGConfig, dry_run: bool = False, datestr: str | None = None): self.config: CGConfig = config self.housekeeper_api: HousekeeperAPI = config.housekeeper_api self.lims_api: LimsAPI = config.lims_api diff --git a/cg/meta/upload/gisaid/gisaid.py b/cg/meta/upload/gisaid/gisaid.py index 0b6f76d48d..6389ccd714 100644 --- a/cg/meta/upload/gisaid/gisaid.py +++ b/cg/meta/upload/gisaid/gisaid.py @@ -3,7 +3,6 @@ import re import tempfile from pathlib import Path -from typing import Optional import pandas as pd from housekeeper.store.models import File @@ -47,7 +46,7 @@ def __init__(self, config: CGConfig): def get_completion_file_from_hk(self, case_id: str) -> File: """Find completon file in Housekeeper and return it""" - completion_file: Optional[File] = self.housekeeper_api.get_file_from_latest_version( + completion_file: File | None = self.housekeeper_api.get_file_from_latest_version( bundle_name=case_id, tags=["komplettering"] ) if not completion_file: diff --git a/cg/meta/upload/gisaid/models.py b/cg/meta/upload/gisaid/models.py index b6b33e8818..aad8103ec6 100644 --- a/cg/meta/upload/gisaid/models.py +++ b/cg/meta/upload/gisaid/models.py @@ -1,6 +1,6 @@ from datetime import datetime from pathlib import Path -from typing import Any, Optional +from typing import Any from pydantic import BaseModel, ConfigDict, model_validator @@ -14,8 +14,8 @@ class FastaFile(BaseModel): class GisaidAccession(BaseModel): log_message: str - accession_nr: Optional[str] = None - sample_id: Optional[str] = None + accession_nr: str | None = None + sample_id: str | None = None @model_validator(mode="before") @classmethod @@ -51,20 +51,20 @@ class GisaidSample(BaseModel): fn: str covv_collection_date: str covv_subm_sample_id: str - covv_virus_name: Optional[str] = None - covv_orig_lab: Optional[str] = None - covv_type: Optional[str] = "betacoronavirus" - covv_passage: Optional[str] = "Original" - covv_location: Optional[str] = None - covv_host: Optional[str] = "Human" - covv_gender: Optional[str] = "unknown" - covv_patient_age: Optional[str] = "unknown" - covv_patient_status: Optional[str] = "unknown" - covv_seq_technology: Optional[str] = "Illumina NovaSeq" - covv_orig_lab_addr: Optional[str] = None - covv_subm_lab: Optional[str] = "Karolinska University Hospital" - covv_subm_lab_addr: Optional[str] = "171 76 Stockholm, Sweden" - covv_authors: Optional[str] = " ,".join(AUTHORS) + covv_virus_name: str | None = None + covv_orig_lab: str | None = None + covv_type: str | None = "betacoronavirus" + covv_passage: str | None = "Original" + covv_location: str | None = None + covv_host: str | None = "Human" + covv_gender: str | None = "unknown" + covv_patient_age: str | None = "unknown" + covv_patient_status: str | None = "unknown" + covv_seq_technology: str | None = "Illumina NovaSeq" + covv_orig_lab_addr: str | None = None + covv_subm_lab: str | None = "Karolinska University Hospital" + covv_subm_lab_addr: str | None = "171 76 Stockholm, Sweden" + covv_authors: str | None = " ,".join(AUTHORS) @model_validator(mode="before") @classmethod diff --git a/cg/meta/upload/nipt/models.py b/cg/meta/upload/nipt/models.py index c964171523..97c14cc96f 100644 --- a/cg/meta/upload/nipt/models.py +++ b/cg/meta/upload/nipt/models.py @@ -1,12 +1,10 @@ -from typing import Optional - from pydantic import BaseModel class StatinaUploadFiles(BaseModel): result_file: str - multiqc_report: Optional[str] = None - segmental_calls: Optional[str] = None + multiqc_report: str | None = None + segmental_calls: str | None = None class FlowCellQ30AndReads(BaseModel): diff --git a/cg/meta/upload/nipt/nipt.py b/cg/meta/upload/nipt/nipt.py index 770e44b617..bb10ca8fa9 100644 --- a/cg/meta/upload/nipt/nipt.py +++ b/cg/meta/upload/nipt/nipt.py @@ -2,7 +2,6 @@ import datetime as dt import logging from pathlib import Path -from typing import Optional import paramiko import requests @@ -76,7 +75,7 @@ def flowcell_passed_qc_value(self, case_id: str, q30_threshold: float) -> bool: LOG.debug(f"Flow cell {flow_cell.name} passed QC for case {case_id}.") return True - def get_housekeeper_results_file(self, case_id: str, tags: Optional[list] = None) -> str: + def get_housekeeper_results_file(self, case_id: str, tags: list | None = None) -> str: """Get the result file for a NIPT analysis from Housekeeper""" if not tags: diff --git a/cg/meta/upload/scout/hk_tags.py b/cg/meta/upload/scout/hk_tags.py index 98f16c6a37..0e96703e58 100644 --- a/cg/meta/upload/scout/hk_tags.py +++ b/cg/meta/upload/scout/hk_tags.py @@ -1,80 +1,71 @@ """Maps tag info from housekeeper tags to scout load config""" -from typing import Optional from pydantic import BaseModel, Field class CaseTags(BaseModel): - snv_vcf: Optional[set[str]] = Field( + snv_vcf: set[str] | None = Field( None, description="vcf_snv for rare disease and vcf_cancer for cancer" ) - snv_research_vcf: Optional[set[str]] = Field( - None, description="vcf_snv_research for rare disease" - ) - sv_vcf: Optional[set[str]] = Field( + snv_research_vcf: set[str] | None = Field(None, description="vcf_snv_research for rare disease") + sv_vcf: set[str] | None = Field( None, description="vcf_cancer_sv for rare disease and vcf_sv_cancer for cancer" ) - sv_research_vcf: Optional[set[str]] = Field( - None, description="vcf_sv_research for rare disease" - ) + sv_research_vcf: set[str] | None = Field(None, description="vcf_sv_research for rare disease") vcf_str: set[str] = Field( None, description="Short Tandem Repeat variants, only for rare disease" ) - cnv_report: Optional[set[str]] = Field( + cnv_report: set[str] | None = Field( None, description="AscatNgs visualization report for cancer" ) - smn_tsv: Optional[set[str]] = Field( - None, description="SMN gene variants, only for rare disease" - ) + smn_tsv: set[str] | None = Field(None, description="SMN gene variants, only for rare disease") peddy_ped: set[str] = Field(None, description="Ped info from peddy, only for rare disease") - peddy_sex: Optional[set[str]] = Field( - None, description="Peddy sex check, only for rare disease" - ) + peddy_sex: set[str] | None = Field(None, description="Peddy sex check, only for rare disease") peddy_check: set[str] = Field(None, description="Peddy pedigree check, only for rare disease") - multiqc_report: Optional[set[str]] = Field(None, description="MultiQC report") - delivery_report: Optional[set[str]] = Field(None, description="Delivery report") - str_catalog: Optional[set[str]] = Field( + multiqc_report: set[str] | None = Field(None, description="MultiQC report") + delivery_report: set[str] | None = Field(None, description="Delivery report") + str_catalog: set[str] | None = Field( None, description="Variant catalog used with expansionhunter" ) gene_fusion: set[str] = Field( None, description="Arriba report for RNA fusions containing only clinical fusions" ) - gene_fusion_report_research: Optional[set[str]] = Field( + gene_fusion_report_research: set[str] | None = Field( None, description="Arriba report for RNA fusions containing all fusions" ) - RNAfusion_report: Optional[set[str]] = Field( + RNAfusion_report: set[str] | None = Field( None, description="Main RNA fusion report containing only clinical fusions" ) - RNAfusion_report_research: Optional[set[str]] = Field( + RNAfusion_report_research: set[str] | None = Field( None, description="Main RNA fusion report containing all fusions" ) - RNAfusion_inspector: Optional[set[str]] = Field( + RNAfusion_inspector: set[str] | None = Field( None, description="RNAfusion inspector report containing only clinical fusions" ) - RNAfusion_inspector_research: Optional[set[str]] = Field( + RNAfusion_inspector_research: set[str] | None = Field( None, description="RNAfusion inspector report containing all fusions" ) - multiqc_rna: Optional[set[str]] = Field(None, description="MultiQC report for RNA samples") - vcf_mei: Optional[set[str]] = Field( + multiqc_rna: set[str] | None = Field(None, description="MultiQC report for RNA samples") + vcf_mei: set[str] | None = Field( None, description="VCF with mobile element insertions, clinical" ) - vcf_mei_research: Optional[set[str]] = Field( + vcf_mei_research: set[str] | None = Field( None, description="VCF with mobile element insertions, research" ) class SampleTags(BaseModel): # If cram does not exist - bam_file: Optional[set[str]] = None - alignment_file: Optional[set[str]] = None - vcf2cytosure: Optional[set[str]] = None - mt_bam: Optional[set[str]] = None - chromograph_autozyg: Optional[set[str]] = None - chromograph_coverage: Optional[set[str]] = None - chromograph_regions: Optional[set[str]] = None - chromograph_sites: Optional[set[str]] = None - reviewer_alignment: Optional[set[str]] = None - reviewer_alignment_index: Optional[set[str]] = None - reviewer_vcf: Optional[set[str]] = None - mitodel_file: Optional[set[str]] = None + bam_file: set[str] | None = None + alignment_file: set[str] | None = None + vcf2cytosure: set[str] | None = None + mt_bam: set[str] | None = None + chromograph_autozyg: set[str] | None = None + chromograph_coverage: set[str] | None = None + chromograph_regions: set[str] | None = None + chromograph_sites: set[str] | None = None + reviewer_alignment: set[str] | None = None + reviewer_alignment_index: set[str] | None = None + reviewer_vcf: set[str] | None = None + mitodel_file: set[str] | None = None diff --git a/cg/meta/upload/scout/mip_config_builder.py b/cg/meta/upload/scout/mip_config_builder.py index 1e2205e213..0a141efd2d 100644 --- a/cg/meta/upload/scout/mip_config_builder.py +++ b/cg/meta/upload/scout/mip_config_builder.py @@ -1,7 +1,6 @@ import logging import re from pathlib import Path -from typing import Optional from housekeeper.store.models import Version @@ -163,7 +162,7 @@ def include_sample_files(self, config_sample: ScoutMipIndividual) -> None: ) @staticmethod - def extract_generic_filepath(file_path: Optional[str]) -> Optional[str]: + def extract_generic_filepath(file_path: str | None) -> str | None: """Remove a file's suffix and identifying integer or X/Y Example: `/some/path/gatkcomb_rhocall_vt_af_chromograph_sites_X.png` becomes diff --git a/cg/meta/upload/scout/scout_config_builder.py b/cg/meta/upload/scout/scout_config_builder.py index 04e0c1faab..c330fe5eda 100644 --- a/cg/meta/upload/scout/scout_config_builder.py +++ b/cg/meta/upload/scout/scout_config_builder.py @@ -1,6 +1,5 @@ """Functions that handle files in the context of scout uploading""" import logging -from typing import Optional import requests from housekeeper.store.models import File, Version @@ -160,19 +159,19 @@ def include_sample_alignment_file(self, config_sample: ScoutIndividual) -> None: hk_tags=self.sample_tags.alignment_file, sample_id=sample_id ) - def get_sample_file(self, hk_tags: set[str], sample_id: str) -> Optional[str]: + def get_sample_file(self, hk_tags: set[str], sample_id: str) -> str | None: """Return a file that is specific for a individual from housekeeper""" tags: set = hk_tags.copy() tags.add(sample_id) return self.get_file_from_hk(hk_tags=tags) - def get_file_from_hk(self, hk_tags: set[str], latest: Optional[bool] = False) -> Optional[str]: + def get_file_from_hk(self, hk_tags: set[str], latest: bool | None = False) -> str | None: """Get a file from housekeeper and return the path as a string.""" LOG.info(f"Get file with tags {hk_tags}") if not hk_tags: LOG.debug("No tags provided, skipping") return None - hk_file: Optional[File] = ( + hk_file: File | None = ( HousekeeperAPI.get_latest_file_from_version(version=self.hk_version_obj, tags=hk_tags) if latest else HousekeeperAPI.get_file_from_version(version=self.hk_version_obj, tags=hk_tags) diff --git a/cg/meta/upload/scout/uploadscoutapi.py b/cg/meta/upload/scout/uploadscoutapi.py index 521b509dc3..f88940b72a 100644 --- a/cg/meta/upload/scout/uploadscoutapi.py +++ b/cg/meta/upload/scout/uploadscoutapi.py @@ -2,7 +2,6 @@ import logging from pathlib import Path -from typing import Optional from housekeeper.store.models import File, Version from pydantic.dataclasses import dataclass @@ -97,7 +96,7 @@ def add_scout_config_to_hk( LOG.info(f"Adding load config {config_file_path} to Housekeeper.") tag_name: str = self.get_load_config_tag() version: Version = self.housekeeper.last_version(case_id) - uploaded_config_file: Optional[File] = self.housekeeper.get_latest_file_from_version( + uploaded_config_file: File | None = self.housekeeper.get_latest_file_from_version( version=version, tags={tag_name} ) if uploaded_config_file: @@ -117,7 +116,7 @@ def add_scout_config_to_hk( def get_multiqc_html_report( self, case_id: str, pipeline: Pipeline - ) -> tuple[ScoutCustomCaseReportTags, Optional[File]]: + ) -> tuple[ScoutCustomCaseReportTags, File | None]: """Return a multiqc report for a case in Housekeeper.""" if pipeline == Pipeline.MIP_RNA: return ( @@ -129,7 +128,7 @@ def get_multiqc_html_report( self.housekeeper.files(bundle=case_id, tags=HK_MULTIQC_HTML_TAG).first(), ) - def get_fusion_report(self, case_id: str, research: bool) -> Optional[File]: + def get_fusion_report(self, case_id: str, research: bool) -> File | None: """Return a fusion report for a case in housekeeper.""" tags = {"fusion"} @@ -140,11 +139,11 @@ def get_fusion_report(self, case_id: str, research: bool) -> Optional[File]: return self.housekeeper.get_file_from_latest_version(bundle_name=case_id, tags=tags) - def get_splice_junctions_bed(self, case_id: str, sample_id: str) -> Optional[File]: + def get_splice_junctions_bed(self, case_id: str, sample_id: str) -> File | None: """Return a splice junctions bed file for a case in Housekeeper.""" tags: set[str] = {"junction", "bed", sample_id} - splice_junctions_bed: Optional[File] = None + splice_junctions_bed: File | None = None try: splice_junctions_bed = self.housekeeper.get_file_from_latest_version( bundle_name=case_id, tags=tags @@ -154,7 +153,7 @@ def get_splice_junctions_bed(self, case_id: str, sample_id: str) -> Optional[Fil return splice_junctions_bed - def get_rna_coverage_bigwig(self, case_id: str, sample_id: str) -> Optional[File]: + def get_rna_coverage_bigwig(self, case_id: str, sample_id: str) -> File | None: """Return an RNA coverage bigwig file for a case in Housekeeper.""" tags: set[str] = {"coverage", "bigwig", sample_id} @@ -177,7 +176,7 @@ def upload_fusion_report_to_scout( report_type: str = "Research" if research else "Clinical" - fusion_report: Optional[File] = self.get_fusion_report(case_id, research) + fusion_report: File | None = self.get_fusion_report(case_id, research) if fusion_report is None: raise FileNotFoundError( f"{report_type} fusion report was not found in Housekeeper for {case_id}." @@ -256,7 +255,7 @@ def upload_rna_coverage_bigwig_to_scout(self, case_id: str, dry_run: bool) -> No for rna_dna_collection in rna_dna_collections: rna_sample_internal_id: str = rna_dna_collection.rna_sample_internal_id dna_sample_name: str = rna_dna_collection.dna_sample_name - rna_coverage_bigwig: Optional[File] = self.get_rna_coverage_bigwig( + rna_coverage_bigwig: File | None = self.get_rna_coverage_bigwig( case_id=case_id, sample_id=rna_sample_internal_id ) @@ -294,7 +293,7 @@ def upload_splice_junctions_bed_to_scout(self, dry_run: bool, case_id: str) -> N for rna_dna_collection in rna_dna_collections: rna_sample_internal_id: str = rna_dna_collection.rna_sample_internal_id dna_sample_name: str = rna_dna_collection.dna_sample_name - splice_junctions_bed: Optional[File] = self.get_splice_junctions_bed( + splice_junctions_bed: File | None = self.get_splice_junctions_bed( case_id=case_id, sample_id=rna_sample_internal_id ) @@ -455,9 +454,9 @@ def filter_cases_related_to_dna_sample( @staticmethod def _get_application_prep_category( subject_id_samples: list[Sample], - ) -> list[Optional[Sample]]: + ) -> list[Sample | None]: """Filter a models Sample list, returning DNA samples selected on their preparation category.""" - subject_id_dna_samples: list[Optional[Sample]] = [ + subject_id_dna_samples: list[Sample | None] = [ sample for sample in subject_id_samples if sample.prep_category diff --git a/cg/meta/workflow/analysis.py b/cg/meta/workflow/analysis.py index c0b9704089..d4e1b2e86c 100644 --- a/cg/meta/workflow/analysis.py +++ b/cg/meta/workflow/analysis.py @@ -4,7 +4,6 @@ import shutil from pathlib import Path from subprocess import CalledProcessError -from typing import Optional, Union import click from housekeeper.store.models import Bundle, Version @@ -92,7 +91,7 @@ def get_workflow_manager(self) -> str: """Get workflow manager for a given pipeline.""" return WorkflowManager.Slurm.value - def get_case_path(self, case_id: str) -> Union[list[Path], Path]: + def get_case_path(self, case_id: str) -> list[Path] | Path: """Path to case working directory.""" raise NotImplementedError @@ -115,7 +114,7 @@ def link_fastq_files(self, case_id: str, dry_run: bool = False) -> None: """ raise NotImplementedError - def get_bundle_deliverables_type(self, case_id: str) -> Optional[str]: + def get_bundle_deliverables_type(self, case_id: str) -> str | None: return None @staticmethod @@ -227,9 +226,7 @@ def get_pipeline_version(self, case_id: str) -> str: LOG.warning("Could not retrieve %s workflow version!", self.pipeline) return "0.0.0" - def set_statusdb_action( - self, case_id: str, action: Optional[str], dry_run: bool = False - ) -> None: + def set_statusdb_action(self, case_id: str, action: str | None, dry_run: bool = False) -> None: """ Set one of the allowed actions on a case in StatusDB. """ @@ -331,7 +328,7 @@ def link_fastq_files_for_sample( self.fastq_handler.concatenate(linked_reads_paths[read], concatenated_paths[read]) self.fastq_handler.remove_files(value) - def get_target_bed_from_lims(self, case_id: str) -> Optional[str]: + def get_target_bed_from_lims(self, case_id: str) -> str | None: """Get target bed filename from LIMS.""" case: Case = self.status_db.get_case_by_internal_id(internal_id=case_id) sample: Sample = case.links[0].sample @@ -342,7 +339,7 @@ def get_target_bed_from_lims(self, case_id: str) -> Optional[str]: target_bed_shortname: str = self.lims_api.capture_kit(lims_id=sample.internal_id) if not target_bed_shortname: return None - bed_version: Optional[BedVersion] = self.status_db.get_bed_version_by_short_name( + bed_version: BedVersion | None = self.status_db.get_bed_version_by_short_name( bed_version_short_name=target_bed_shortname ) if not bed_version: @@ -422,7 +419,7 @@ def get_date_from_file_path(file_path: Path) -> dt.datetime.date: """ return dt.datetime.fromtimestamp(int(os.path.getctime(file_path))) - def get_additional_naming_metadata(self, sample_obj: Sample) -> Optional[str]: + def get_additional_naming_metadata(self, sample_obj: Sample) -> str | None: return None def get_latest_metadata(self, case_id: str) -> AnalysisModel: @@ -445,7 +442,7 @@ def clean_analyses(self, case_id: str) -> None: analysis_obj.cleaned_at = analysis_obj.cleaned_at or dt.datetime.now() self.status_db.session.commit() - def clean_run_dir(self, case_id: str, yes: bool, case_path: Union[list[Path], Path]) -> int: + def clean_run_dir(self, case_id: str, yes: bool, case_path: list[Path] | Path) -> int: """Remove workflow run directory.""" try: diff --git a/cg/meta/workflow/balsamic.py b/cg/meta/workflow/balsamic.py index 5ce662722c..4e91d55fd8 100644 --- a/cg/meta/workflow/balsamic.py +++ b/cg/meta/workflow/balsamic.py @@ -1,8 +1,7 @@ -"""Module for Balsamic Analysis API""" +"""Module for Balsamic Analysis API.""" import logging from pathlib import Path -from typing import Optional, Union from housekeeper.store.models import File, Version from pydantic.v1 import ValidationError @@ -186,7 +185,7 @@ def get_sample_type(sample_obj: Sample) -> SampleType: return SampleType.TUMOR return SampleType.NORMAL - def get_derived_bed(self, panel_bed: str) -> Optional[Path]: + def get_derived_bed(self, panel_bed: str) -> Path | None: """Returns the verified capture kit path or the derived panel BED path.""" if not panel_bed: return None @@ -206,7 +205,7 @@ def get_derived_bed(self, panel_bed: str) -> Optional[Path]: ) return derived_panel_bed - def get_verified_bed(self, panel_bed: str, sample_data: dict) -> Optional[str]: + def get_verified_bed(self, panel_bed: str, sample_data: dict) -> str | None: """Takes a dict with samples and attributes. Retrieves unique attributes for application type and target_bed. Verifies that those attributes are the same across multiple samples, @@ -220,7 +219,7 @@ def get_verified_bed(self, panel_bed: str, sample_data: dict) -> Optional[str]: - When bed file required for analysis, but is not set or cannot be retrieved. """ - panel_bed: Optional[Path] = self.get_derived_bed(panel_bed) + panel_bed: Path | None = self.get_derived_bed(panel_bed) application_types = {v["application_type"].lower() for k, v in sample_data.items()} target_beds: set = {v["target_bed"] for k, v in sample_data.items()} @@ -242,7 +241,7 @@ def get_verified_bed(self, panel_bed: str, sample_data: dict) -> Optional[str]: ) return Path(self.bed_path, target_bed).as_posix() - def get_verified_pon(self, panel_bed: str, pon_cnn: str) -> Optional[str]: + def get_verified_pon(self, panel_bed: str, pon_cnn: str) -> str | None: """Returns the validated PON or extracts the latest one available if it is not provided Raises BalsamicStartError: @@ -264,7 +263,7 @@ def get_verified_pon(self, panel_bed: str, pon_cnn: str) -> Optional[str]: return latest_pon - def get_latest_pon_file(self, panel_bed: str) -> Optional[str]: + def get_latest_pon_file(self, panel_bed: str) -> str | None: """Returns the latest PON cnn file associated to a specific capture bed""" if not panel_bed: @@ -280,8 +279,8 @@ def get_latest_pon_file(self, panel_bed: str) -> Optional[str]: return sorted_pon_files[0].as_posix() if sorted_pon_files else None @staticmethod - def get_verified_gender(sample_data: dict) -> Union[Gender.FEMALE, Gender.MALE]: - """Takes a dict with samples and attributes, and returns a verified case gender provided by the customer""" + def get_verified_gender(sample_data: dict) -> str: + """Takes a dict with samples and attributes, and returns a verified case gender provided by the customer.""" gender = next(iter(sample_data.values()))["gender"] @@ -340,7 +339,7 @@ def get_verified_samples(self, case_id: str, sample_data: dict) -> dict[str, str "normal": normal_sample_path, } - def get_latest_raw_file_data(self, case_id: str, tags: list) -> Union[dict, list]: + def get_latest_raw_file_data(self, case_id: str, tags: list) -> dict | list: """Retrieves the data of the latest file associated to a specific case ID and a list of tags.""" version: Version = self.housekeeper_api.last_version(bundle=case_id) @@ -402,7 +401,7 @@ def parse_analysis(self, config_raw: dict, qc_metrics_raw: dict, **kwargs) -> Ba @staticmethod def cast_metrics_type( sequencing_type: str, metrics: dict - ) -> Union[BalsamicTargetedQCMetrics, BalsamicWGSQCMetrics]: + ) -> BalsamicTargetedQCMetrics | BalsamicWGSQCMetrics: """Cast metrics model type according to the sequencing type""" if metrics: @@ -416,7 +415,7 @@ def cast_metrics_type( return metrics @staticmethod - def get_latest_file_by_pattern(directory: Path, pattern: str) -> Optional[str]: + def get_latest_file_by_pattern(directory: Path, pattern: str) -> str | None: """Returns the latest file (--.vcf.gz) matching a pattern from a specific directory.""" available_files: iter = sorted( Path(directory).glob(f"*{pattern}*.vcf.gz"), @@ -442,7 +441,7 @@ def get_parsed_observation_file_paths(self, observations: list[str]) -> dict: return verified_observations - def get_swegen_verified_path(self, variants: Variants) -> Optional[str]: + def get_swegen_verified_path(self, variants: Variants) -> str | None: """Return verified SweGen path.""" swegen_file: str = self.get_latest_file_by_pattern( directory=self.swegen_path, pattern=variants @@ -456,7 +455,7 @@ def get_verified_config_case_arguments( panel_bed: str, pon_cnn: str, observations: list[str] = None, - gender: Optional[str] = None, + gender: str | None = None, ) -> dict: """Takes a dictionary with per-sample parameters, validates them, and transforms into command line arguments @@ -510,7 +509,7 @@ def print_sample_params(case_id: str, sample_data: dict) -> None: ) LOG.info("") - def get_sample_params(self, case_id: str, panel_bed: Optional[str]) -> dict: + def get_sample_params(self, case_id: str, panel_bed: str | None) -> dict: """Returns a dictionary of attributes for each sample in given family, where SAMPLE ID is used as key""" @@ -537,9 +536,7 @@ def get_case_application_type(self, case_id: str) -> str: if application_types: return application_types.pop().lower() - def resolve_target_bed( - self, panel_bed: Optional[str], link_object: CaseSample - ) -> Optional[str]: + 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: @@ -605,7 +602,7 @@ def run_analysis( self, case_id: str, run_analysis: bool = True, - slurm_quality_of_service: Optional[str] = None, + slurm_quality_of_service: str | None = None, dry_run: bool = False, ) -> None: """Execute BALSAMIC run analysis with given options""" diff --git a/cg/meta/workflow/fastq.py b/cg/meta/workflow/fastq.py index c6d736b1e8..5a6055a032 100644 --- a/cg/meta/workflow/fastq.py +++ b/cg/meta/workflow/fastq.py @@ -12,7 +12,6 @@ import re import shutil from pathlib import Path -from typing import Optional LOG = logging.getLogger(__name__) @@ -171,8 +170,8 @@ def create_fastq_name( read: str, date: dt.datetime = DEFAULT_DATE_STR, index: str = DEFAULT_INDEX, - undetermined: Optional[str] = None, - meta: Optional[str] = None, + undetermined: str | None = None, + meta: str | None = None, ) -> str: """Name a FASTQ file with standard conventions and no naming constrains from pipeline.""" @@ -190,8 +189,8 @@ def create_fastq_name( read: str, date: dt.datetime = DEFAULT_DATE_STR, index: str = DEFAULT_INDEX, - undetermined: Optional[str] = None, - meta: Optional[str] = None, + undetermined: str | None = None, + meta: str | None = None, ) -> str: """Name a FASTQ file following Balsamic conventions. Naming must be xxx_R_1.fastq.gz and xxx_R_2.fastq.gz""" @@ -209,8 +208,8 @@ def create_fastq_name( read: str, date: dt.datetime = DEFAULT_DATE_STR, index: str = DEFAULT_INDEX, - undetermined: Optional[str] = None, - meta: Optional[str] = None, + undetermined: str | None = None, + meta: str | None = None, ) -> str: """Name a FASTQ file following MIP conventions.""" flowcell = f"{flowcell}-undetermined" if undetermined else flowcell @@ -227,8 +226,8 @@ def create_fastq_name( read: str, date: dt.datetime = DEFAULT_DATE_STR, index: str = DEFAULT_INDEX, - undetermined: Optional[str] = None, - meta: Optional[str] = None, + undetermined: str | None = None, + meta: str | None = None, ) -> str: """Name a FASTQ file following usalt conventions. Naming must be xxx_R_1.fastq.gz and xxx_R_2.fastq.gz""" @@ -247,8 +246,8 @@ def create_fastq_name( read: str, date: dt.datetime = DEFAULT_DATE_STR, index: str = DEFAULT_INDEX, - undetermined: Optional[str] = None, - meta: Optional[str] = None, + undetermined: str | None = None, + meta: str | None = None, ) -> str: """Name a FASTQ file following mutant conventions. Naming must be xxx_R_1.fastq.gz and xxx_R_2.fastq.gz""" @@ -274,7 +273,7 @@ def create_nanopore_fastq_name( flowcell: str, sample: str, filenr: str, - meta: Optional[str] = None, + meta: str | None = None, ) -> str: return f"{flowcell}_{sample}_{meta}_{filenr}.fastq.gz" diff --git a/cg/meta/workflow/fluffy.py b/cg/meta/workflow/fluffy.py index adc6c81919..98b3110863 100644 --- a/cg/meta/workflow/fluffy.py +++ b/cg/meta/workflow/fluffy.py @@ -3,7 +3,6 @@ import shutil from enum import Enum from pathlib import Path -from typing import Optional from pydantic import BaseModel from sqlalchemy.orm import Query @@ -48,9 +47,9 @@ class FluffySample(BaseModel): index: str index2: str sample_name: str - control: Optional[str] = "N" - recipe: Optional[str] = "R1" - operator: Optional[str] = "script" + control: str | None = "N" + recipe: str | None = "R1" + operator: str | None = "script" sample_project: str exclude: bool library_nM: float @@ -169,7 +168,7 @@ def get_concentrations_from_lims(self, sample_id: str) -> str: """Get sample concentration from LIMS""" return self.lims_api.get_sample_attribute(lims_id=sample_id, key="concentration_sample") - def get_sample_sequenced_date(self, sample_id: str) -> Optional[dt.date]: + def get_sample_sequenced_date(self, sample_id: str) -> dt.date | None: sample_obj: Sample = self.status_db.get_sample_by_internal_id(sample_id) last_sequenced_at: dt.datetime = sample_obj.last_sequenced_at if last_sequenced_at: diff --git a/cg/meta/workflow/microsalt.py b/cg/meta/workflow/microsalt.py index 33f8f2fae0..e51283303f 100644 --- a/cg/meta/workflow/microsalt.py +++ b/cg/meta/workflow/microsalt.py @@ -12,7 +12,7 @@ import shutil from datetime import datetime from pathlib import Path -from typing import Any, Optional, Union +from typing import Any import click @@ -72,7 +72,7 @@ def get_case_path(self, case_id: str) -> list[Path]: return sorted(case_directories, key=os.path.getctime, reverse=True) - def get_latest_case_path(self, case_id: str) -> Union[Path, None]: + def get_latest_case_path(self, case_id: str) -> Path | None: """Return latest run dir for a microbial case, if no path found it returns None.""" lims_project: str = self.get_project( self.status_db.get_case_by_internal_id(internal_id=case_id).links[0].sample.internal_id @@ -87,7 +87,7 @@ def get_latest_case_path(self, case_id: str) -> Union[Path, None]: None, ) - def clean_run_dir(self, case_id: str, yes: bool, case_path: Union[list[Path], Path]) -> int: + def clean_run_dir(self, case_id: str, yes: bool, case_path: list[Path] | Path) -> int: """Remove workflow run directories for a MicroSALT case.""" if not case_path: @@ -148,15 +148,13 @@ def get_deliverables_file_path(self, case_id: str) -> Path: def get_sample_fastq_destination_dir(self, case: Case, sample: Sample) -> Path: return Path(self.get_case_fastq_path(case_id=case.internal_id), sample.internal_id) - def link_fastq_files( - self, case_id: str, sample_id: Optional[str], dry_run: bool = False - ) -> None: + def link_fastq_files(self, case_id: str, sample_id: str | None, dry_run: bool = False) -> None: case_obj: Case = self.status_db.get_case_by_internal_id(internal_id=case_id) samples: list[Sample] = self.get_samples(case_id=case_id, sample_id=sample_id) for sample_obj in samples: self.link_fastq_files_for_sample(case_obj=case_obj, sample_obj=sample_obj) - def get_samples(self, case_id: str, sample_id: Optional[str] = None) -> list[Sample]: + def get_samples(self, case_id: str, sample_id: str | None = None) -> list[Sample]: """Returns a list of samples to configure If sample_id is specified, will return a list with only this sample_id. Otherwise, returns all samples in given case""" @@ -236,7 +234,7 @@ def get_project(self, sample_id: str) -> str: def resolve_case_sample_id( self, sample: bool, ticket: bool, unique_id: Any - ) -> tuple[str, Optional[str]]: + ) -> tuple[str, str | None]: """Resolve case_id and sample_id w based on input arguments.""" if ticket and sample: LOG.error("Flags -t and -s are mutually exclusive!") @@ -291,7 +289,7 @@ def microsalt_qc(self, case_id: str, run_dir_path: Path, lims_project: str) -> b for sample_id in case_qc: sample: Sample = self.status_db.get_sample_by_internal_id(internal_id=sample_id) - sample_check: Union[dict, None] = self.qc_sample_check( + sample_check: dict | None = self.qc_sample_check( sample=sample, sample_qc=case_qc[sample_id], ) @@ -339,7 +337,7 @@ def create_qc_done_file(self, run_dir_path: Path, failed_samples: dict) -> None: """Creates a QC_done when a QC check is performed.""" write_json(file_path=run_dir_path.joinpath("QC_done.json"), content=failed_samples) - def qc_sample_check(self, sample: Sample, sample_qc: dict) -> Union[dict, None]: + def qc_sample_check(self, sample: Sample, sample_qc: dict) -> dict | None: """Perform a QC on a sample.""" if sample.control == ControlEnum.negative: reads_pass: bool = self.check_external_negative_control_sample(sample) diff --git a/cg/meta/workflow/mip.py b/cg/meta/workflow/mip.py index 63b796b192..404cb2850a 100644 --- a/cg/meta/workflow/mip.py +++ b/cg/meta/workflow/mip.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Optional, Union +from typing import Any from pydantic.v1 import ValidationError @@ -85,13 +85,13 @@ def get_sample_info_path(self, case_id: str) -> Path: """Get case analysis sample info path""" return Path(self.root, case_id, "analysis", f"{case_id}_qc_sample_info.yaml") - def get_panel_bed(self, panel_bed: str = None) -> Optional[str]: + def get_panel_bed(self, panel_bed: str = None) -> str | None: """Check and return BED gene panel.""" if not panel_bed: return None if panel_bed.endswith(FileExtensions.BED): return panel_bed - bed_version: Optional[BedVersion] = self.status_db.get_bed_version_by_short_name( + bed_version: BedVersion | None = self.status_db.get_bed_version_by_short_name( bed_version_short_name=panel_bed ) if not bed_version: @@ -127,7 +127,7 @@ def write_pedigree_config(self, case_id: str, data: dict) -> None: LOG.info("Config file saved to %s", pedigree_config_path) @staticmethod - def get_sample_data(link_obj: CaseSample) -> dict[str, Union[str, int]]: + def get_sample_data(link_obj: CaseSample) -> dict[str, str | int]: """Return sample specific data.""" return { "sample_id": link_obj.sample.internal_id, diff --git a/cg/meta/workflow/mip_dna.py b/cg/meta/workflow/mip_dna.py index bfa486062d..42dd112fb5 100644 --- a/cg/meta/workflow/mip_dna.py +++ b/cg/meta/workflow/mip_dna.py @@ -1,5 +1,3 @@ -from typing import Optional, Union - from cg.constants import DEFAULT_CAPTURE_KIT, Pipeline from cg.constants.constants import AnalysisType from cg.constants.gene_panel import GENOME_BUILD_37 @@ -50,14 +48,14 @@ def process(self) -> Process: return self._process def config_sample( - self, link_obj: CaseSample, panel_bed: Optional[str] - ) -> dict[str, Union[str, int, None]]: + self, link_obj: CaseSample, panel_bed: str | None + ) -> dict[str, str | int | None]: """Return config sample data.""" - sample_data: dict[str, Union[str, int]] = self.get_sample_data(link_obj=link_obj) + sample_data: dict[str, str | int] = self.get_sample_data(link_obj=link_obj) if sample_data["analysis_type"] == AnalysisType.WHOLE_GENOME_SEQUENCING: sample_data["capture_kit"]: str = panel_bed or DEFAULT_CAPTURE_KIT else: - sample_data["capture_kit"]: Optional[str] = panel_bed or self.get_target_bed_from_lims( + sample_data["capture_kit"]: str | None = panel_bed or self.get_target_bed_from_lims( case_id=link_obj.case.internal_id ) if link_obj.mother: diff --git a/cg/meta/workflow/mip_rna.py b/cg/meta/workflow/mip_rna.py index 75d6b73b2b..98751c3702 100644 --- a/cg/meta/workflow/mip_rna.py +++ b/cg/meta/workflow/mip_rna.py @@ -1,5 +1,3 @@ -from typing import Optional, Union - from cg.constants import Pipeline from cg.constants.gene_panel import GENOME_BUILD_38 from cg.constants.pedigree import Pedigree @@ -48,10 +46,8 @@ def process(self) -> Process: ) return self._process - def config_sample( - self, link_obj, panel_bed: Optional[str] = None - ) -> dict[str, Union[str, int]]: - sample_data: dict[str, Union[str, int]] = self.get_sample_data(link_obj) + def config_sample(self, link_obj, panel_bed: str | None = None) -> dict[str, str | int]: + sample_data: dict[str, str | int] = self.get_sample_data(link_obj) if link_obj.mother: sample_data[Pedigree.MOTHER.value]: str = link_obj.mother.internal_id if link_obj.father: diff --git a/cg/meta/workflow/mutant.py b/cg/meta/workflow/mutant.py index 10a997fd0c..b60c99607c 100644 --- a/cg/meta/workflow/mutant.py +++ b/cg/meta/workflow/mutant.py @@ -1,7 +1,6 @@ import logging import shutil from pathlib import Path -from typing import Optional from cg.constants import Pipeline from cg.constants.constants import FileFormat @@ -141,7 +140,7 @@ def create_case_config(self, case_id: str, dry_run: bool) -> None: ) LOG.info("Saved config to %s", config_path) - def get_additional_naming_metadata(self, sample_obj: Sample) -> Optional[str]: + def get_additional_naming_metadata(self, sample_obj: Sample) -> str | None: sample_name = sample_obj.name region_code = self.lims_api.get_sample_attribute( lims_id=sample_obj.internal_id, key="region_code" diff --git a/cg/meta/workflow/nf_analysis.py b/cg/meta/workflow/nf_analysis.py index 5f56420941..1ab376a5a1 100644 --- a/cg/meta/workflow/nf_analysis.py +++ b/cg/meta/workflow/nf_analysis.py @@ -2,7 +2,7 @@ import operator from datetime import datetime from pathlib import Path -from typing import Any, Optional +from typing import Any from cg.constants import Pipeline from cg.constants.constants import FileExtensions, FileFormat, WorkflowManager @@ -26,19 +26,19 @@ class NfAnalysisAPI(AnalysisAPI): def __init__(self, config: CGConfig, pipeline: Pipeline): super().__init__(config=config, pipeline=pipeline) self.pipeline: Pipeline = pipeline - self.root_dir: Optional[str] = None - self.nfcore_pipeline_path: Optional[str] = None - self.references: Optional[str] = None - self.profile: Optional[str] = None - self.conda_env: Optional[str] = None - self.conda_binary: Optional[str] = None - self.tower_binary_path: Optional[str] = None - self.tower_pipeline: Optional[str] = None - self.account: Optional[str] = None - self.email: Optional[str] = None - self.compute_env: Optional[str] = None - self.revision: Optional[str] = None - self.nextflow_binary_path: Optional[str] = None + self.root_dir: str | None = None + self.nfcore_pipeline_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.tower_binary_path: str | None = None + self.tower_pipeline: str | None = None + self.account: str | None = None + self.email: str | None = None + self.compute_env: str | None = None + self.revision: str | None = None + self.nextflow_binary_path: str | None = None @property def root(self) -> str: @@ -56,7 +56,7 @@ def process(self): def process(self, process: Process): self._process = process - def get_profile(self, profile: Optional[str] = None) -> str: + def get_profile(self, profile: str | None = None) -> str: """Get NF profiles.""" return profile or self.profile @@ -79,7 +79,7 @@ def get_sample_sheet_path(self, case_id: str) -> Path: ) @staticmethod - def get_nextflow_config_path(nextflow_config: Optional[str] = None) -> Optional[Path]: + def get_nextflow_config_path(nextflow_config: str | None = None) -> Path | None: """Path to Nextflow config file.""" if nextflow_config: return Path(nextflow_config).absolute() @@ -100,7 +100,7 @@ def get_metrics_deliverables_path(self, case_id: str) -> Path: FileExtensions.YAML ) - def get_params_file_path(self, case_id: str, params_file: Optional[Path] = None) -> Path: + def get_params_file_path(self, case_id: str, params_file: Path | None = None) -> Path: """Return parameters file or a path where the default parameters file for a case id should be located.""" if params_file: return Path(params_file).absolute() @@ -123,7 +123,7 @@ def get_log_path(self, case_id: str, pipeline: str, log: str = None) -> Path: f"{case_id}_{pipeline}_nextflow_{launch_time}", ).with_suffix(FileExtensions.LOG) - def get_workdir_path(self, case_id: str, work_dir: Optional[Path] = None) -> Path: + def get_workdir_path(self, case_id: str, work_dir: Path | None = None) -> Path: """Path to NF work directory.""" if work_dir: return work_dir.absolute() diff --git a/cg/meta/workflow/rnafusion.py b/cg/meta/workflow/rnafusion.py index fcf1afeab7..2486cef76b 100644 --- a/cg/meta/workflow/rnafusion.py +++ b/cg/meta/workflow/rnafusion.py @@ -2,7 +2,7 @@ import logging from pathlib import Path -from typing import Any, Optional +from typing import Any from cg import resources from cg.constants import Pipeline @@ -103,7 +103,7 @@ def get_sample_sheet_content(self, case_id: str, strandedness: Strandedness) -> return content_per_sample def get_pipeline_parameters( - self, case_id: str, genomes_base: Optional[Path] = None + self, case_id: str, genomes_base: Path | None = None ) -> RnafusionParameters: """Get Rnafusion parameters.""" LOG.debug("Getting parameters information") @@ -115,7 +115,7 @@ def get_pipeline_parameters( priority=self.account, ) - def get_references_path(self, genomes_base: Optional[Path] = None) -> Path: + def get_references_path(self, genomes_base: Path | None = None) -> Path: if genomes_base: return genomes_base.absolute() return Path(self.references).absolute() diff --git a/cg/models/balsamic/config.py b/cg/models/balsamic/config.py index 543abdc45c..fc3cd40a26 100644 --- a/cg/models/balsamic/config.py +++ b/cg/models/balsamic/config.py @@ -1,6 +1,5 @@ from datetime import datetime from pathlib import Path -from typing import Optional, Union from pydantic.v1 import BaseModel, validator @@ -22,7 +21,7 @@ class BalsamicConfigAnalysis(BaseModel): analysis_workflow: str sequencing_type: str BALSAMIC_version: str - config_creation_date: Union[datetime, str] + config_creation_date: datetime | str class BalsamicConfigSample(BaseModel): @@ -48,10 +47,10 @@ class BalsamicConfigReference(BaseModel): """ reference_genome: Path - reference_genome_version: Optional[str] + reference_genome_version: str | None @validator("reference_genome_version", always=True) - def extract_genome_version_from_path(cls, value: Optional[str], values: dict) -> str: + def extract_genome_version_from_path(cls, value: str | None, values: dict) -> str: """ Returns the genome version from the reference path: /home/proj/stage/cancer/balsamic_cache/X.X.X/hg19/genome/human_g1k_v37.fasta @@ -70,7 +69,7 @@ class BalsamicConfigPanel(BaseModel): """ capture_kit: str - capture_kit_version: Optional[str] + capture_kit_version: str | None chrom: list[str] @validator("capture_kit", pre=True) @@ -80,7 +79,7 @@ def extract_capture_kit_name_from_path(cls, capture_kit: str) -> str: @validator("capture_kit_version", always=True) def extract_capture_kit_name_from_name( - cls, capture_kit_version: Optional[str], values: dict + cls, capture_kit_version: str | None, values: dict ) -> str: """Return the panel bed version from its filename (e.g. gicfdna_3.1_hg19_design.bed).""" return values["capture_kit"].split("_")[-3] @@ -100,12 +99,12 @@ class BalsamicConfigQC(BaseModel): """ picard_rmdup: bool - adapter: Optional[str] + adapter: str | None quality_trim: bool adapter_trim: bool umi_trim: bool - min_seq_length: Optional[str] - umi_trim_length: Optional[str] + min_seq_length: str | None + umi_trim_length: str | None class BalsamicVarCaller(BaseModel): @@ -139,7 +138,7 @@ class BalsamicConfigJSON(BaseModel): analysis: BalsamicConfigAnalysis samples: dict[str, BalsamicConfigSample] reference: BalsamicConfigReference - panel: Optional[BalsamicConfigPanel] + panel: BalsamicConfigPanel | None QC: BalsamicConfigQC vcf: dict[str, BalsamicVarCaller] bioinfo_tools_version: dict[str, list[str]] diff --git a/cg/models/balsamic/metrics.py b/cg/models/balsamic/metrics.py index 502e1ab157..e1ef9b4f7b 100644 --- a/cg/models/balsamic/metrics.py +++ b/cg/models/balsamic/metrics.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic.v1 import BaseModel, validator from cg.models.deliverables.metric_deliverables import MetricCondition, MetricsBase @@ -18,29 +16,29 @@ class BalsamicMetricsBase(MetricsBase): condition: balsamic metric validation condition """ - condition: Optional[MetricCondition] + condition: MetricCondition | None class BalsamicQCMetrics(BaseModel): """BALSAMIC common QC metrics""" - mean_insert_size: Optional[float] - fold_80_base_penalty: Optional[float] + mean_insert_size: float | None + fold_80_base_penalty: float | None class BalsamicTargetedQCMetrics(BalsamicQCMetrics): """BALSAMIC targeted QC metrics""" - mean_target_coverage: Optional[float] - median_target_coverage: Optional[float] - percent_duplication: Optional[float] - pct_target_bases_50x: Optional[float] - pct_target_bases_100x: Optional[float] - pct_target_bases_250x: Optional[float] - pct_target_bases_500x: Optional[float] - pct_target_bases_1000x: Optional[float] - pct_off_bait: Optional[float] - gc_dropout: Optional[float] + mean_target_coverage: float | None + median_target_coverage: float | None + percent_duplication: float | None + pct_target_bases_50x: float | None + pct_target_bases_100x: float | None + pct_target_bases_250x: float | None + pct_target_bases_500x: float | None + pct_target_bases_1000x: float | None + pct_off_bait: float | None + gc_dropout: float | None _pct_values = validator( "percent_duplication", @@ -57,14 +55,14 @@ class BalsamicTargetedQCMetrics(BalsamicQCMetrics): class BalsamicWGSQCMetrics(BalsamicQCMetrics): """BALSAMIC WGS QC metrics""" - median_coverage: Optional[float] - percent_duplication_r1: Optional[float] - percent_duplication_r2: Optional[float] - pct_15x: Optional[float] - pct_30x: Optional[float] - pct_60x: Optional[float] - pct_100x: Optional[float] - pct_pf_reads_improper_pairs: Optional[float] + median_coverage: float | None + percent_duplication_r1: float | None + percent_duplication_r2: float | None + pct_15x: float | None + pct_30x: float | None + pct_60x: float | None + pct_100x: float | None + pct_pf_reads_improper_pairs: float | None _pct_values = validator( "pct_15x", diff --git a/cg/models/cg_config.py b/cg/models/cg_config.py index 20aeb0d16b..4932c61ebc 100644 --- a/cg/models/cg_config.py +++ b/cg/models/cg_config.py @@ -1,5 +1,4 @@ import logging -from typing import Optional from pydantic.v1 import BaseModel, EmailStr, Field from typing_extensions import Literal @@ -34,11 +33,11 @@ class Sequencers(BaseModel): class SlurmConfig(BaseModel): account: str - hours: Optional[int] + hours: int | None mail_user: EmailStr - memory: Optional[int] - number_tasks: Optional[int] - conda_env: Optional[str] + memory: int | None + number_tasks: int | None + conda_env: str | None qos: SlurmQos = SlurmQos.LOW @@ -78,7 +77,7 @@ class TrailblazerConfig(BaseModel): class StatinaConfig(BaseModel): - host: Optional[str] + host: str | None user: str key: str api_url: str @@ -88,7 +87,7 @@ class StatinaConfig(BaseModel): class CommonAppConfig(BaseModel): binary_path: str - config_path: Optional[str] + config_path: str | None class FluffyUploadConfig(BaseModel): @@ -111,7 +110,7 @@ class LimsConfig(BaseModel): class CrunchyConfig(BaseModel): - conda_binary: Optional[str] = None + conda_binary: str | None = None cram_reference: str slurm: SlurmConfig @@ -134,13 +133,13 @@ class BalsamicConfig(CommonAppConfig): class MutantConfig(BaseModel): binary_path: str - conda_binary: Optional[str] = None + conda_binary: str | None = None conda_env: str root: str class MipConfig(BaseModel): - conda_binary: Optional[str] = None + conda_binary: str | None = None conda_env: str mip_config: str pipeline: str @@ -156,7 +155,7 @@ class RnafusionConfig(CommonAppConfig): conda_env: str compute_env: str profile: str - conda_binary: Optional[str] = None + conda_binary: str | None = None launch_directory: str revision: str slurm: SlurmConfig @@ -171,7 +170,7 @@ class TaxprofilerConfig(CommonAppConfig): profile: str pipeline_path: str revision: str - conda_binary: Optional[str] = None + conda_binary: str | None = None hostremoval_reference: str databases: str slurm: SlurmConfig @@ -181,7 +180,7 @@ class TaxprofilerConfig(CommonAppConfig): class MicrosaltConfig(BaseModel): binary_path: str - conda_binary: Optional[str] = None + conda_binary: str | None = None conda_env: str queries_path: str root: str @@ -245,7 +244,7 @@ class CGConfig(BaseModel): environment: Literal["production", "stage"] = "stage" flow_cells_dir: str madeline_exe: str - max_flowcells: Optional[int] + max_flowcells: int | None data_input: DataInput | None = None # Base APIs that always should exist status_db_: Store = None @@ -259,7 +258,7 @@ class CGConfig(BaseModel): crunchy: CrunchyConfig = None crunchy_api_: CrunchyAPI = None data_delivery: DataDeliveryConfig = Field(None, alias="data-delivery") - data_flow_config: Optional[DataFlowConfig] = None + data_flow_config: DataFlowConfig | None = None demultiplex: DemultiplexConfig = None demultiplex_api_: DemultiplexingAPI = None encryption: Encryption | None = None @@ -280,19 +279,19 @@ class CGConfig(BaseModel): madeline_api_: MadelineAPI = None mutacc_auto: MutaccAutoConfig = Field(None, alias="mutacc-auto") mutacc_auto_api_: MutaccAutoAPI = None - pigz: Optional[CommonAppConfig] = None - pdc: Optional[CommonAppConfig] = None - pdc_api_: Optional[PdcAPI] + pigz: CommonAppConfig | None = None + pdc: CommonAppConfig | None = None + pdc_api_: PdcAPI | None scout: CommonAppConfig = None scout_api_: ScoutAPI = None - tar: Optional[CommonAppConfig] = None + tar: CommonAppConfig | None = None trailblazer: TrailblazerConfig = None trailblazer_api_: TrailblazerAPI = None # Meta APIs that will use the apps from CGConfig balsamic: BalsamicConfig = None statina: StatinaConfig = None - fohm: Optional[FOHMConfig] = None + fohm: FOHMConfig | None = None fluffy: FluffyConfig = None microsalt: MicrosaltConfig = None gisaid: GisaidConfig = None diff --git a/cg/models/deliverables/metric_deliverables.py b/cg/models/deliverables/metric_deliverables.py index 8adc2613e2..c4b2fb0050 100644 --- a/cg/models/deliverables/metric_deliverables.py +++ b/cg/models/deliverables/metric_deliverables.py @@ -1,5 +1,5 @@ import operator -from typing import Any, Callable, Optional +from typing import Any, Callable from pydantic.v1 import BaseModel, Field, validator @@ -75,13 +75,13 @@ def validate_operator(cls, norm: str) -> str: class MetricsBase(BaseModel): """Definition for elements in deliverables metrics file.""" - header: Optional[str] + header: str | None id: str input: str name: str step: str value: Any - condition: Optional[MetricCondition] + condition: MetricCondition | None class SampleMetric(BaseModel): @@ -122,7 +122,7 @@ class MetricsDeliverables(BaseModel): """Specification for a metric general deliverables file""" metrics_: list[MetricsBase] = Field(..., alias="metrics") - sample_ids: Optional[set] + sample_ids: set | None @validator("sample_ids", always=True) def set_sample_ids(cls, _, values: dict) -> set: @@ -161,5 +161,5 @@ def validate_metrics(cls, metrics: list[MetricsBase]) -> list[MetricsBase]: class MultiqcDataJson(BaseModel): """Multiqc data json model.""" - report_general_stats_data: Optional[list[dict]] - report_data_sources: Optional[dict] + report_general_stats_data: list[dict] | None + report_data_sources: dict | None diff --git a/cg/models/demultiplex/run_parameters.py b/cg/models/demultiplex/run_parameters.py index 131101c087..8ca9cdd223 100644 --- a/cg/models/demultiplex/run_parameters.py +++ b/cg/models/demultiplex/run_parameters.py @@ -1,7 +1,6 @@ """Module for modeling run parameters file parsing.""" import logging from pathlib import Path -from typing import Optional from xml.etree import ElementTree from cg.constants.demultiplexing import RunParametersXMLNodes @@ -30,7 +29,7 @@ def index_length(self) -> int: return index_one_length @staticmethod - def node_not_found(node: Optional[ElementTree.Element], name: str) -> None: + def node_not_found(node: ElementTree.Element | None, name: str) -> None: """Raise exception if the given node is not found.""" if node is None: message = f"Could not determine {name}" @@ -44,19 +43,19 @@ def validate_instrument(self) -> None: ) @property - def control_software_version(self) -> Optional[str]: + def control_software_version(self) -> str | None: """Return the control software version if existent.""" raise NotImplementedError( "Impossible to retrieve control software version from parent class" ) @property - def reagent_kit_version(self) -> Optional[str]: + def reagent_kit_version(self) -> str | None: """Return the reagent kit version if existent.""" raise NotImplementedError("Impossible to retrieve reagent kit version from parent class") @property - def sequencer(self) -> Optional[str]: + def sequencer(self) -> str | None: """Return the sequencer associated with the current run parameters.""" raise NotImplementedError("Impossible to retrieve sequencer from parent class") @@ -95,7 +94,7 @@ class RunParametersNovaSeq6000(RunParameters): def validate_instrument(self) -> None: """Raise an error if the class was not instantiated with a NovaSeq6000 file.""" node_name: str = RunParametersXMLNodes.APPLICATION - xml_node: Optional[ElementTree.Element] = self.tree.find(node_name) + xml_node: ElementTree.Element | None = self.tree.find(node_name) self.node_not_found(node=xml_node, name="Instrument") if xml_node.text != RunParametersXMLNodes.NOVASEQ_6000_APPLICATION: raise RunParametersError( @@ -106,7 +105,7 @@ def validate_instrument(self) -> None: def control_software_version(self) -> str: """Return the control software version.""" node_name: str = RunParametersXMLNodes.APPLICATION_VERSION - xml_node: Optional[ElementTree.Element] = self.tree.find(node_name) + xml_node: ElementTree.Element | None = self.tree.find(node_name) self.node_not_found(node=xml_node, name="control software version") return xml_node.text @@ -114,7 +113,7 @@ def control_software_version(self) -> str: def reagent_kit_version(self) -> str: """Return the reagent kit version if existent, return 'unknown' otherwise.""" node_name: str = RunParametersXMLNodes.REAGENT_KIT_VERSION - xml_node: Optional[ElementTree.Element] = self.tree.find(node_name) + xml_node: ElementTree.Element | None = self.tree.find(node_name) if xml_node is None: LOG.warning("Could not determine reagent kit version") LOG.info("Set reagent kit version to 'unknown'") @@ -159,7 +158,7 @@ class RunParametersNovaSeqX(RunParameters): def validate_instrument(self) -> None: """Raise an error if the class was not instantiated with a NovaSeqX file.""" node_name: str = RunParametersXMLNodes.INSTRUMENT_TYPE - xml_node: Optional[ElementTree.Element] = self.tree.find(node_name) + xml_node: ElementTree.Element | None = self.tree.find(node_name) self.node_not_found(node=xml_node, name="Instrument") if xml_node.text != RunParametersXMLNodes.NOVASEQ_X_INSTRUMENT: raise RunParametersError("The file parsed does not correspond to a NovaSeqX instrument") @@ -183,7 +182,7 @@ def sequencer(self) -> str: def read_parser(self) -> dict[str, int]: """Return read and index cycle values parsed as a dictionary.""" cycle_mapping: dict[str, int] = {} - planned_reads: Optional[ElementTree.Element] = self.tree.find( + planned_reads: ElementTree.Element | None = self.tree.find( RunParametersXMLNodes.PLANNED_READS ) self.node_not_found(node=planned_reads, name="PlannedReads") diff --git a/cg/models/demultiplex/sbatch.py b/cg/models/demultiplex/sbatch.py index 6e91b7baf8..22f36f77c1 100644 --- a/cg/models/demultiplex/sbatch.py +++ b/cg/models/demultiplex/sbatch.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import BaseModel from typing_extensions import Literal @@ -15,7 +13,7 @@ class SbatchError(BaseModel): class SbatchCommand(BaseModel): run_dir: str # path/to/a_flowcell/ demux_dir: str # path/to/output_dir/ - unaligned_dir: Optional[str] = None # path/to/output_dir/Unaligned/ + unaligned_dir: str | None = None # path/to/output_dir/Unaligned/ sample_sheet: str # path/to/SampleSheet.csv demux_completed_file: str # path/to/demuxcomplete.txt environment: Literal["stage", "production"] diff --git a/cg/models/email.py b/cg/models/email.py index a169e0fa9d..cb8a6c7941 100644 --- a/cg/models/email.py +++ b/cg/models/email.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from cg.models.cg_config import EmailBaseSettings @@ -9,4 +8,4 @@ class EmailInfo(EmailBaseSettings): sender_email: str = "hiseq.clinical@hasta.scilifelab.se" subject: str message: str - file: Optional[Path] + file: Path | None diff --git a/cg/models/flow_cell/flow_cell.py b/cg/models/flow_cell/flow_cell.py index 27506a3674..daec2fe0e8 100644 --- a/cg/models/flow_cell/flow_cell.py +++ b/cg/models/flow_cell/flow_cell.py @@ -2,8 +2,7 @@ import datetime import logging from pathlib import Path -from typing import Optional, Type, Union -from cg.models.flow_cell.utils import parse_date +from typing import Type from pydantic import ValidationError from typing_extensions import Literal @@ -27,6 +26,7 @@ RunParametersNovaSeq6000, RunParametersNovaSeqX, ) +from cg.models.flow_cell.utils import parse_date LOG = logging.getLogger(__name__) @@ -34,19 +34,19 @@ class FlowCellDirectoryData: """Class to collect information about flow cell directories and their particular files.""" - def __init__(self, flow_cell_path: Path, bcl_converter: Optional[str] = None): + def __init__(self, flow_cell_path: Path, bcl_converter: str | None = None): LOG.debug(f"Instantiating FlowCellDirectoryData with path {flow_cell_path}") self.path: Path = flow_cell_path self.machine_name: str = "" - self._run_parameters: Optional[RunParameters] = None + self._run_parameters: RunParameters | None = None self.run_date: datetime.datetime = datetime.datetime.now() self.machine_number: int = 0 self.base_name: str = "" # Base name is flow cell-id + flow cell position self.id: str = "" self.position: Literal["A", "B"] = "A" self.parse_flow_cell_dir_name() - self.bcl_converter: Optional[str] = self.get_bcl_converter(bcl_converter) - self._sample_sheet_path_hk: Optional[Path] = None + self.bcl_converter: str | None = self.get_bcl_converter(bcl_converter) + self._sample_sheet_path_hk: Path | None = None def parse_flow_cell_dir_name(self): """Parse relevant information from flow cell name. @@ -119,7 +119,7 @@ def run_parameters(self) -> RunParameters: @property def sample_type( self, - ) -> Union[Type[FlowCellSampleBcl2Fastq], Type[FlowCellSampleBCLConvert]]: + ) -> Type[FlowCellSampleBcl2Fastq] | Type[FlowCellSampleBCLConvert]: """Return the sample class used in the flow cell.""" if self.bcl_converter == BclConverter.BCL2FASTQ: return FlowCellSampleBcl2Fastq diff --git a/cg/models/invoice/invoice.py b/cg/models/invoice/invoice.py index 226055d8ab..53848f1150 100644 --- a/cg/models/invoice/invoice.py +++ b/cg/models/invoice/invoice.py @@ -1,5 +1,5 @@ """Module for defining invoice models.""" -from typing import Any, Optional +from typing import Any from pydantic.v1 import BaseModel @@ -31,26 +31,26 @@ class InvoiceInfo(BaseModel): name: str id: str - lims_id: Optional[str] + lims_id: str | None application_tag: str project: str - date: Optional[Any] + date: Any | None price: int priority: PriorityTerms - price_kth: Optional[int] - total_price: Optional[int] + price_kth: int | None + total_price: int | None class InvoiceReport(BaseModel): """Class that collects information used to create the invoice Excel sheet.""" cost_center: str - project_number: Optional[str] + project_number: str | None customer_id: str customer_name: str - agreement: Optional[str] + agreement: str | None invoice_id: int contact: dict records: list - pooled_samples: Optional[Any] + pooled_samples: Any | None record_type: RecordType diff --git a/cg/models/lims/sample.py b/cg/models/lims/sample.py index 876fe2002b..29d7b3c5c0 100644 --- a/cg/models/lims/sample.py +++ b/cg/models/lims/sample.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic.v1 import BaseModel, validator from typing_extensions import Literal @@ -10,47 +8,47 @@ class Udf(BaseModel): application: str - capture_kit: Optional[str] - collection_date: Optional[str] - comment: Optional[str] - concentration: Optional[str] - concentration_sample: Optional[str] + capture_kit: str | None + collection_date: str | None + comment: str | None + concentration: str | None + concentration_sample: str | None customer: str - control: Optional[str] - data_analysis: Optional[str] - data_delivery: Optional[str] - elution_buffer: Optional[str] - extraction_method: Optional[str] + control: str | None + data_analysis: str | None + data_delivery: str | None + elution_buffer: str | None + extraction_method: str | None family_name: str = "NA" - formalin_fixation_time: Optional[str] - index: Optional[str] - index_number: Optional[str] - lab_code: Optional[str] - organism: Optional[str] - organism_other: Optional[str] - original_lab: Optional[str] - original_lab_address: Optional[str] - pool: Optional[str] - post_formalin_fixation_time: Optional[str] - pre_processing_method: Optional[str] - primer: Optional[str] + formalin_fixation_time: str | None + index: str | None + index_number: str | None + lab_code: str | None + organism: str | None + organism_other: str | None + original_lab: str | None + original_lab_address: str | None + pool: str | None + post_formalin_fixation_time: str | None + pre_processing_method: str | None + primer: str | None priority: str = Priority.standard.name - quantity: Optional[str] - reference_genome: Optional[str] - region: Optional[str] - region_code: Optional[str] + quantity: str | None + reference_genome: str | None + region: str | None + region_code: str | None require_qc_ok: bool = False - rml_plate_name: Optional[str] - selection_criteria: Optional[str] + rml_plate_name: str | None + selection_criteria: str | None sex: Literal["M", "F", "unknown"] = "unknown" - skip_reception_control: Optional[bool] = None + skip_reception_control: bool | None = None source: str = "NA" - tissue_block_size: Optional[str] - tumour: Optional[bool] = False - tumour_purity: Optional[str] - volume: Optional[str] - well_position_rml: Optional[str] - verified_organism: Optional[bool] + tissue_block_size: str | None + tumour: bool | None = False + tumour_purity: str | None + volume: str | None + well_position_rml: str | None + verified_organism: bool | None @validator("sex", pre=True) def validate_sex(cls, value: str): @@ -60,10 +58,10 @@ def validate_sex(cls, value: str): class LimsSample(BaseModel): name: str container: str = "Tube" - container_name: Optional[str] - well_position: Optional[str] - index_sequence: Optional[str] - udfs: Optional[Udf] + container_name: str | None + well_position: str | None + index_sequence: str | None + udfs: Udf | None @classmethod def parse_obj(cls, obj: dict): diff --git a/cg/models/mip/mip_metrics_deliverables.py b/cg/models/mip/mip_metrics_deliverables.py index 028a81a355..993664a454 100644 --- a/cg/models/mip/mip_metrics_deliverables.py +++ b/cg/models/mip/mip_metrics_deliverables.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from pydantic.v1 import validator @@ -70,13 +70,13 @@ class MIPMetricsDeliverables(MetricsDeliverables): "MEDIAN_TARGET_COVERAGE": MedianTargetCoverage, "gender": GenderCheck, } - duplicate_reads: Optional[list[DuplicateReads]] - mapped_reads: Optional[list[MIPMappedReads]] - mean_insert_size: Optional[list[MeanInsertSize]] - median_target_coverage: Optional[list[MedianTargetCoverage]] - predicted_sex: Optional[list[GenderCheck]] + duplicate_reads: list[DuplicateReads] | None + mapped_reads: list[MIPMappedReads] | None + mean_insert_size: list[MeanInsertSize] | None + median_target_coverage: list[MedianTargetCoverage] | None + predicted_sex: list[GenderCheck] | None sample_metric_to_parse: list[str] = SAMPLE_METRICS_TO_PARSE - sample_id_metrics: Optional[list[MIPParsedMetrics]] + sample_id_metrics: list[MIPParsedMetrics] | None @validator("duplicate_reads", always=True) def set_duplicate_reads(cls, _, values: dict) -> list[DuplicateReads]: diff --git a/cg/models/mip/mip_sample_info.py b/cg/models/mip/mip_sample_info.py index cf68f7ca62..67f4957f41 100644 --- a/cg/models/mip/mip_sample_info.py +++ b/cg/models/mip/mip_sample_info.py @@ -1,6 +1,5 @@ """Model MIP sample info""" import datetime -from typing import Optional from pydantic.v1 import BaseModel, Field, validator @@ -8,8 +7,8 @@ class MipBaseSampleInfo(BaseModel): """This model is used when validating the mip sample info file""" - family_id_: Optional[str] = Field(None, alias="family_id") - case_id: Optional[str] + family_id_: str | None = Field(None, alias="family_id") + case_id: str | None human_genome_build_: dict = Field(..., alias="human_genome_build") genome_build: str = None program_: dict = Field(None, alias="program") diff --git a/cg/models/nf_analysis.py b/cg/models/nf_analysis.py index 8bebd2b7b2..bce67d5ca3 100644 --- a/cg/models/nf_analysis.py +++ b/cg/models/nf_analysis.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional, Union from pydantic.v1 import BaseModel, Field, conlist, validator @@ -48,12 +47,12 @@ class FileDeliverable(BaseModel): id: str format: str path: str - path_index: Optional[str] + path_index: str | None step: str tag: str @validator("path", "path_index", pre=True) - def path_exist(cls, file_path: Union[str, Path]) -> Optional[str]: + def path_exist(cls, file_path: str | Path) -> str | None: if file_path is not None: path = Path(file_path) if not path.exists(): diff --git a/cg/models/observations/input_files.py b/cg/models/observations/input_files.py index 29d1fb3d39..6929d15098 100644 --- a/cg/models/observations/input_files.py +++ b/cg/models/observations/input_files.py @@ -1,6 +1,5 @@ """Loqusdb input files models.""" import logging -from typing import Optional from pydantic import BaseModel, FilePath @@ -11,7 +10,7 @@ class ObservationsInputFiles(BaseModel): """Model for validating Loqusdb input files.""" snv_vcf_path: FilePath - sv_vcf_path: Optional[FilePath] = None + sv_vcf_path: FilePath | None = None class MipDNAObservationsInputFiles(ObservationsInputFiles): @@ -25,4 +24,4 @@ class BalsamicObservationsInputFiles(ObservationsInputFiles): """Model for validating cancer Loqusdb input files.""" snv_germline_vcf_path: FilePath - sv_germline_vcf_path: Optional[FilePath] = None + sv_germline_vcf_path: FilePath | None = None diff --git a/cg/models/orders/excel_sample.py b/cg/models/orders/excel_sample.py index fa76d90f42..bdc293f4e2 100644 --- a/cg/models/orders/excel_sample.py +++ b/cg/models/orders/excel_sample.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import AfterValidator, BeforeValidator, Field from typing_extensions import Annotated @@ -65,7 +63,7 @@ class ExcelSample(OrderSample): organism_other: str = Field(None, alias=ExcelSampleAliases.ORGANISM_OTHER) original_lab: str = Field(None, alias=ExcelSampleAliases.ORIGINAL_LAB) original_lab_address: str = Field(None, alias=ExcelSampleAliases.ORIGINAL_LAB_ADDRESS) - panels: Annotated[Optional[list[str]], BeforeValidator(parse_panels)] = Field( + panels: Annotated[list[str] | None, BeforeValidator(parse_panels)] = Field( None, alias=ExcelSampleAliases.PANELS ) pool: str = Field(None, alias=ExcelSampleAliases.POOL) diff --git a/cg/models/orders/json_sample.py b/cg/models/orders/json_sample.py index 23dde66893..db775ed3f2 100644 --- a/cg/models/orders/json_sample.py +++ b/cg/models/orders/json_sample.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import BeforeValidator, constr from typing_extensions import Annotated @@ -9,16 +7,16 @@ class JsonSample(OrderSample): - cohorts: Optional[list[str]] = None - concentration: Optional[str] = None - concentration_sample: Optional[str] = None - control: Optional[str] = None + cohorts: list[str] | None = None + concentration: str | None = None + concentration_sample: str | None = None + control: str | None = None data_analysis: Pipeline = Pipeline.MIP_DNA data_delivery: DataDelivery = DataDelivery.SCOUT - index: Optional[str] = None - panels: Optional[list[str]] = None - quantity: Optional[str] = None - synopsis: Annotated[Optional[str], BeforeValidator(join_list)] = None + index: str | None = None + panels: list[str] | None = None + quantity: str | None = None + synopsis: Annotated[str | None, BeforeValidator(join_list)] = None well_position: Annotated[ - Optional[constr(pattern=r"^[A-H]:(1[0-2]|[1-9])$")], BeforeValidator(convert_well) + constr(pattern=r"^[A-H]:(1[0-2]|[1-9])$") | None, BeforeValidator(convert_well) ] = None diff --git a/cg/models/orders/order.py b/cg/models/orders/order.py index b686371699..f8bc7f1079 100644 --- a/cg/models/orders/order.py +++ b/cg/models/orders/order.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from pydantic.v1 import BaseModel, conlist, constr @@ -9,11 +9,11 @@ class OrderIn(BaseModel): name: constr(min_length=2, max_length=Sample.order.property.columns[0].type.length) - comment: Optional[str] + comment: str | None customer: constr(min_length=1, max_length=Customer.internal_id.property.columns[0].type.length) samples: conlist(Any, min_items=1) - skip_reception_control: Optional[bool] = None - ticket: Optional[str] + skip_reception_control: bool | None = None + ticket: str | None @classmethod def parse_obj(cls, obj: dict, project: OrderType) -> "OrderIn": diff --git a/cg/models/orders/orderform_schema.py b/cg/models/orders/orderform_schema.py index 45b80bb8e7..2dfe2c2ffb 100644 --- a/cg/models/orders/orderform_schema.py +++ b/cg/models/orders/orderform_schema.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import BaseModel from cg.models.orders.sample_base import OrderSample @@ -7,31 +5,31 @@ # Class for holding information about cases in order class OrderCase(BaseModel): - cohorts: Optional[list[str]] + cohorts: list[str] | None name: str - panels: Optional[list[str]] + panels: list[str] | None priority: str samples: list[OrderSample] - synopsis: Optional[str] + synopsis: str | None class OrderPool(BaseModel): name: str data_analysis: str - data_delivery: Optional[str] + data_delivery: str | None application: str samples: list[OrderSample] # This is for validating in data class Orderform(BaseModel): - comment: Optional[str] = None + comment: str | None = None delivery_type: str project_type: str customer: str name: str - data_analysis: Optional[str] = None - data_delivery: Optional[str] = None - ticket: Optional[int] = None + data_analysis: str | None = None + data_delivery: str | None = None + ticket: int | None = None samples: list[OrderSample] - cases: Optional[list[OrderCase]] = None + cases: list[OrderCase] | None = None diff --git a/cg/models/orders/sample_base.py b/cg/models/orders/sample_base.py index 60ffae73f4..54cd1c57c4 100644 --- a/cg/models/orders/sample_base.py +++ b/cg/models/orders/sample_base.py @@ -1,5 +1,4 @@ from enum import StrEnum -from typing import Optional from pydantic import BaseModel, BeforeValidator, ConfigDict, constr from typing_extensions import Annotated @@ -48,81 +47,75 @@ class OrderSample(BaseModel): model_config = ConfigDict( arbitrary_types_allowed=True, populate_by_name=True, coerce_numbers_to_str=True ) - age_at_sampling: Optional[str] = None + age_at_sampling: str | None = None application: constr(max_length=Application.tag.property.columns[0].type.length) - capture_kit: Optional[str] = None - collection_date: Optional[str] = None - comment: Optional[constr(max_length=Sample.comment.property.columns[0].type.length)] = None - concentration: Optional[float] = None - concentration_sample: Optional[float] = None - container: Optional[ContainerEnum] = None - container_name: Optional[str] = None - control: Optional[str] = None - customer: Optional[ - constr(max_length=Customer.internal_id.property.columns[0].type.length) - ] = None - custom_index: Optional[str] = None + capture_kit: str | None = None + collection_date: str | None = None + comment: constr(max_length=Sample.comment.property.columns[0].type.length) | None = None + concentration: float | None = None + concentration_sample: float | None = None + container: ContainerEnum | None = None + container_name: str | None = None + control: str | None = None + customer: constr(max_length=Customer.internal_id.property.columns[0].type.length) | None = None + custom_index: str | None = None data_analysis: Pipeline data_delivery: DataDelivery - elution_buffer: Optional[str] = None - extraction_method: Optional[str] = None - family_name: Optional[ - constr( - pattern=NAME_PATTERN, - min_length=2, - max_length=Case.name.property.columns[0].type.length, - ) - ] = None - father: Optional[ - constr(pattern=NAME_PATTERN, max_length=Sample.name.property.columns[0].type.length) - ] = None - formalin_fixation_time: Optional[int] = None - index: Optional[str] = None - index_number: Optional[str] = None - index_sequence: Optional[str] = None - internal_id: Optional[ - constr(max_length=Sample.internal_id.property.columns[0].type.length) - ] = None - lab_code: Optional[str] = None - mother: Optional[ - constr(pattern=NAME_PATTERN, max_length=Sample.name.property.columns[0].type.length) - ] = None + elution_buffer: str | None = None + extraction_method: str | None = None + family_name: constr( + pattern=NAME_PATTERN, + min_length=2, + max_length=Case.name.property.columns[0].type.length, + ) | None = None + father: constr( + pattern=NAME_PATTERN, max_length=Sample.name.property.columns[0].type.length + ) | None = None + formalin_fixation_time: int | None = None + index: str | None = None + index_number: str | None = None + index_sequence: str | None = None + internal_id: constr(max_length=Sample.internal_id.property.columns[0].type.length) | None = None + lab_code: str | None = None + mother: constr( + pattern=NAME_PATTERN, max_length=Sample.name.property.columns[0].type.length + ) | None = None name: constr( pattern=NAME_PATTERN, min_length=2, max_length=Sample.name.property.columns[0].type.length, ) - organism: Optional[str] = None - organism_other: Optional[str] = None - original_lab: Optional[str] = None - original_lab_address: Optional[str] = None - phenotype_groups: Optional[list[str]] = None - phenotype_terms: Optional[list[str]] = None - pool: Optional[constr(max_length=Pool.name.property.columns[0].type.length)] = None - post_formalin_fixation_time: Optional[int] = None - pre_processing_method: Optional[str] = None + organism: str | None = None + organism_other: str | None = None + original_lab: str | None = None + original_lab_address: str | None = None + phenotype_groups: list[str] | None = None + phenotype_terms: list[str] | None = None + pool: constr(max_length=Pool.name.property.columns[0].type.length) | None = None + post_formalin_fixation_time: int | None = None + pre_processing_method: str | None = None priority: Annotated[PriorityEnum, BeforeValidator(snake_case)] = PriorityEnum.standard - primer: Optional[str] = None - quantity: Optional[int] = None - reagent_label: Optional[str] = None - reference_genome: Optional[ - constr(max_length=Sample.reference_genome.property.columns[0].type.length) - ] = None - region: Optional[str] = None - region_code: Optional[str] = None + primer: str | None = None + quantity: int | None = None + reagent_label: str | None = None + reference_genome: constr( + max_length=Sample.reference_genome.property.columns[0].type.length + ) | None = None + region: str | None = None + region_code: str | None = None require_qc_ok: bool = False - rml_plate_name: Optional[str] = None - selection_criteria: Optional[str] = None + rml_plate_name: str | None = None + selection_criteria: str | None = None sex: SexEnum = SexEnum.unknown - source: Optional[str] = None + source: str | None = None status: StatusEnum = StatusEnum.unknown - subject_id: Optional[ - constr(pattern=NAME_PATTERN, max_length=Sample.subject_id.property.columns[0].type.length) - ] = None - tissue_block_size: Optional[str] = None + subject_id: constr( + pattern=NAME_PATTERN, max_length=Sample.subject_id.property.columns[0].type.length + ) | None = None + tissue_block_size: str | None = None tumour: bool = False - tumour_purity: Optional[int] = None - verified_organism: Optional[bool] = None - volume: Optional[str] = None - well_position: Optional[str] = None - well_position_rml: Optional[str] = None + tumour_purity: int | None = None + verified_organism: bool | None = None + volume: str | None = None + well_position: str | None = None + well_position_rml: str | None = None diff --git a/cg/models/orders/samples.py b/cg/models/orders/samples.py index 207c7b853e..c06058a50c 100644 --- a/cg/models/orders/samples.py +++ b/cg/models/orders/samples.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic.v1 import BaseModel, constr, validator from cg.constants import DataDelivery @@ -18,23 +16,23 @@ class OptionalIntValidator: @classmethod - def str_to_int(cls, v: str) -> Optional[int]: + def str_to_int(cls, v: str) -> int | None: return int(v) if v else None class OptionalFloatValidator: @classmethod - def str_to_float(cls, v: str) -> Optional[float]: + def str_to_float(cls, v: str) -> float | None: return float(v) if v else None class OrderInSample(BaseModel): # Order portal specific - internal_id: Optional[constr(max_length=Sample.internal_id.property.columns[0].type.length)] + internal_id: constr(max_length=Sample.internal_id.property.columns[0].type.length) | None _suitable_project: OrderType = None application: constr(max_length=Application.tag.property.columns[0].type.length) - comment: Optional[constr(max_length=Sample.comment.property.columns[0].type.length)] - skip_reception_control: Optional[bool] = None + comment: constr(max_length=Sample.comment.property.columns[0].type.length) | None + skip_reception_control: bool | None = None data_analysis: Pipeline data_delivery: DataDelivery name: constr( @@ -54,61 +52,57 @@ def is_sample_for(cls, project: OrderType): class Of1508Sample(OrderInSample): # Orderform 1508 # Order portal specific - internal_id: Optional[constr(max_length=Sample.internal_id.property.columns[0].type.length)] + internal_id: constr(max_length=Sample.internal_id.property.columns[0].type.length) | None # "required for new samples" - name: Optional[ - constr( - regex=NAME_PATTERN, - min_length=2, - max_length=Sample.name.property.columns[0].type.length, - ) - ] + name: constr( + regex=NAME_PATTERN, + min_length=2, + max_length=Sample.name.property.columns[0].type.length, + ) | None # customer - age_at_sampling: Optional[float] + age_at_sampling: float | None family_name: constr( regex=NAME_PATTERN, min_length=2, max_length=Case.name.property.columns[0].type.length, ) - case_internal_id: Optional[ - constr(max_length=Sample.internal_id.property.columns[0].type.length) - ] + case_internal_id: constr(max_length=Sample.internal_id.property.columns[0].type.length) | None sex: SexEnum = SexEnum.unknown tumour: bool = False - source: Optional[str] - control: Optional[str] - volume: Optional[str] - container: Optional[ContainerEnum] + source: str | None + control: str | None + volume: str | None + container: ContainerEnum | None # "required if plate for new samples" - container_name: Optional[str] - well_position: Optional[str] + container_name: str | None + well_position: str | None # "Required if samples are part of trio/family" - mother: Optional[ - constr(regex=NAME_PATTERN, max_length=Sample.name.property.columns[0].type.length) - ] - father: Optional[ - constr(regex=NAME_PATTERN, max_length=Sample.name.property.columns[0].type.length) - ] + mother: constr( + regex=NAME_PATTERN, max_length=Sample.name.property.columns[0].type.length + ) | None + father: constr( + regex=NAME_PATTERN, max_length=Sample.name.property.columns[0].type.length + ) | None # This information is required for panel analysis - capture_kit: Optional[str] + capture_kit: str | None # This information is required for panel- or exome analysis - elution_buffer: Optional[str] - tumour_purity: Optional[int] + elution_buffer: str | None + tumour_purity: int | None # "This information is optional for FFPE-samples for new samples" - formalin_fixation_time: Optional[int] - post_formalin_fixation_time: Optional[int] - tissue_block_size: Optional[str] + formalin_fixation_time: int | None + post_formalin_fixation_time: int | None + tissue_block_size: str | None # "Not Required" - cohorts: Optional[list[str]] - phenotype_groups: Optional[list[str]] - phenotype_terms: Optional[list[str]] + cohorts: list[str] | None + phenotype_groups: list[str] | None + phenotype_terms: list[str] | None require_qc_ok: bool = False - quantity: Optional[int] - subject_id: Optional[ - constr(regex=NAME_PATTERN, max_length=Sample.subject_id.property.columns[0].type.length) - ] - synopsis: Optional[str] + quantity: int | None + subject_id: constr( + regex=NAME_PATTERN, max_length=Sample.subject_id.property.columns[0].type.length + ) | None + synopsis: str | None @validator("container", "container_name", "name", "source", "subject_id", "volume") def required_for_new_samples(cls, value, values, **kwargs): @@ -123,7 +117,7 @@ def required_for_new_samples(cls, value, values, **kwargs): "quantity", pre=True, ) - def str_to_int(cls, v: str) -> Optional[int]: + def str_to_int(cls, v: str) -> int | None: return OptionalIntValidator.str_to_int(v=v) @validator( @@ -131,7 +125,7 @@ def str_to_int(cls, v: str) -> Optional[int]: "volume", pre=True, ) - def str_to_float(cls, v: str) -> Optional[float]: + def str_to_float(cls, v: str) -> float | None: return OptionalFloatValidator.str_to_float(v=v) @@ -148,7 +142,7 @@ class BalsamicSample(Of1508Sample): class BalsamicQCSample(Of1508Sample): _suitable_project = OrderType.BALSAMIC_QC - reference_genome: Optional[GenomeVersion] + reference_genome: GenomeVersion | None class BalsamicUmiSample(Of1508Sample): @@ -168,21 +162,21 @@ class FastqSample(OrderInSample): # Orderform 1508 # "required" - container: Optional[ContainerEnum] + container: ContainerEnum | None sex: SexEnum = SexEnum.unknown source: str tumour: bool # "required if plate" - container_name: Optional[str] - well_position: Optional[str] + container_name: str | None + well_position: str | None elution_buffer: str # This information is required for panel analysis - capture_kit: Optional[str] + capture_kit: str | None # "Not Required" - quantity: Optional[int] + quantity: int | None @validator("quantity", pre=True) - def str_to_int(cls, v: str) -> Optional[int]: + def str_to_int(cls, v: str) -> int | None: return OptionalIntValidator.str_to_int(v=v) @@ -194,19 +188,19 @@ class RmlSample(OrderInSample): # "This information is required" pool: constr(max_length=Pool.name.property.columns[0].type.length) concentration: float - concentration_sample: Optional[float] + concentration_sample: float | None index: str - index_number: Optional[str] + index_number: str | None # "Required if Plate" - rml_plate_name: Optional[str] - well_position_rml: Optional[str] + rml_plate_name: str | None + well_position_rml: str | None # "Automatically generated (if not custom) or custom" - index_sequence: Optional[str] + index_sequence: str | None # "Not required" - control: Optional[str] + control: str | None @validator("concentration_sample", pre=True) - def str_to_float(cls, v: str) -> Optional[float]: + def str_to_float(cls, v: str) -> float | None: return OptionalFloatValidator.str_to_float(v=v) @@ -220,24 +214,24 @@ class MetagenomeSample(OrderInSample): # 1605 Orderform Microbial Metagenomes- 16S # "This information is required" - container: Optional[ContainerEnum] + container: ContainerEnum | None elution_buffer: str source: str # "Required if Plate" - container_name: Optional[str] - well_position: Optional[str] + container_name: str | None + well_position: str | None # "This information is not required" - concentration_sample: Optional[float] - quantity: Optional[int] - extraction_method: Optional[str] - control: Optional[ControlEnum] + concentration_sample: float | None + quantity: int | None + extraction_method: str | None + control: ControlEnum | None @validator("quantity", pre=True) - def str_to_int(cls, v: str) -> Optional[int]: + def str_to_int(cls, v: str) -> int | None: return OptionalIntValidator.str_to_int(v=v) @validator("concentration_sample", pre=True) - def str_to_float(cls, v: str) -> Optional[float]: + def str_to_float(cls, v: str) -> float | None: return OptionalFloatValidator.str_to_float(v=v) @@ -247,27 +241,25 @@ class MicrobialSample(OrderInSample): organism: constr(max_length=Organism.internal_id.property.columns[0].type.length) reference_genome: constr(max_length=Sample.reference_genome.property.columns[0].type.length) elution_buffer: str - extraction_method: Optional[str] + extraction_method: str | None container: ContainerEnum # "Required if Plate" - container_name: Optional[str] - well_position: Optional[str] + container_name: str | None + well_position: str | None # "Required if "Other" is chosen in column "Species"" - organism_other: Optional[ - constr(max_length=Organism.internal_id.property.columns[0].type.length) - ] + organism_other: constr(max_length=Organism.internal_id.property.columns[0].type.length) | None # "These fields are not required" - concentration_sample: Optional[float] - quantity: Optional[int] - verified_organism: Optional[bool] # sent to LIMS - control: Optional[str] + concentration_sample: float | None + quantity: int | None + verified_organism: bool | None # sent to LIMS + control: str | None @validator("quantity", pre=True) - def str_to_int(cls, v: str) -> Optional[int]: + def str_to_int(cls, v: str) -> int | None: return OptionalIntValidator.str_to_int(v=v) @validator("concentration_sample", pre=True) - def str_to_float(cls, v: str) -> Optional[float]: + def str_to_float(cls, v: str) -> float | None: return OptionalFloatValidator.str_to_float(v=v) @@ -290,7 +282,7 @@ class SarsCov2Sample(MicrobialSample): region: str region_code: str selection_criteria: str - volume: Optional[str] + volume: str | None def sample_class_for(project: OrderType): diff --git a/cg/models/orders/validators/excel_sample_validators.py b/cg/models/orders/validators/excel_sample_validators.py index e5695a8955..2ce895ea04 100644 --- a/cg/models/orders/validators/excel_sample_validators.py +++ b/cg/models/orders/validators/excel_sample_validators.py @@ -1,10 +1,8 @@ -from typing import Optional - from cg.constants.orderforms import REV_SEX_MAP, SOURCE_TYPES from cg.models.orders.sample_base import PriorityEnum -def parse_panels(panels: str) -> Optional[list[str]]: +def parse_panels(panels: str) -> list[str] | None: if not panels: return None separator = ";" if ";" in panels else None @@ -32,7 +30,7 @@ def validate_data_analysis(data_analysis): return data_analysis -def numeric_value(value: Optional[str]) -> Optional[str]: +def numeric_value(value: str | None) -> str | None: """Validates that the given string can be given as either an integer or a float. Also converts floats of the type x.00 to x.""" if not value: @@ -50,13 +48,13 @@ def validate_parent(parent: str) -> str: return None if parent == "0.0" else parent -def validate_source(source: Optional[str]) -> str: +def validate_source(source: str | None) -> str: if source not in SOURCE_TYPES: raise ValueError(f"'{source}' is not a valid source") return source -def convert_sex(sex: str) -> Optional[str]: +def convert_sex(sex: str) -> str | None: if not sex: return None sex = sex.strip() @@ -71,10 +69,10 @@ def replace_spaces_with_underscores(value: str) -> str: return value.replace(" ", "_") -def convert_to_priority(priority: Optional[str]) -> Optional[str]: +def convert_to_priority(priority: str | None) -> str | None: """Translates the Swedish 'förtur' to 'priority' if specified in the order.""" return PriorityEnum.priority if priority == "förtur" else priority -def convert_to_date(date: Optional[str]) -> Optional[str]: +def convert_to_date(date: str | None) -> str | None: return date[:10] if date else None diff --git a/cg/models/report/metadata.py b/cg/models/report/metadata.py index b978090e55..610cea3560 100644 --- a/cg/models/report/metadata.py +++ b/cg/models/report/metadata.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import BaseModel, BeforeValidator from typing_extensions import Annotated diff --git a/cg/models/report/report.py b/cg/models/report/report.py index 4195190ecb..70c958b2e6 100644 --- a/cg/models/report/report.py +++ b/cg/models/report/report.py @@ -1,5 +1,4 @@ import logging -from typing import Optional from pydantic import BaseModel, BeforeValidator, model_validator from typing_extensions import Annotated @@ -31,7 +30,7 @@ class CustomerModel(BaseModel): name: Annotated[str, BeforeValidator(get_report_string)] = NA_FIELD id: Annotated[str, BeforeValidator(get_report_string)] = NA_FIELD invoice_address: Annotated[str, BeforeValidator(get_report_string)] = NA_FIELD - scout_access: Optional[bool] = None + scout_access: bool | None = None class ScoutReportFiles(BaseModel): @@ -131,4 +130,4 @@ class ReportModel(BaseModel): version: Annotated[str, BeforeValidator(get_report_string)] = NA_FIELD date: Annotated[str, BeforeValidator(get_date_as_string)] = NA_FIELD case: CaseModel - accredited: Optional[bool] = None + accredited: bool | None = None diff --git a/cg/models/report/sample.py b/cg/models/report/sample.py index e969f467eb..e378f661d5 100644 --- a/cg/models/report/sample.py +++ b/cg/models/report/sample.py @@ -1,5 +1,3 @@ -from typing import Optional, Union - from pydantic import BaseModel, BeforeValidator from typing_extensions import Annotated @@ -40,8 +38,8 @@ class ApplicationModel(BaseModel): details: Annotated[str, BeforeValidator(get_report_string)] = NA_FIELD limitations: Annotated[str, BeforeValidator(get_report_string)] = NA_FIELD pipeline_limitations: Annotated[str, BeforeValidator(get_report_string)] = NA_FIELD - accredited: Optional[bool] = None - external: Optional[bool] = None + accredited: bool | None = None + external: bool | None = None class MethodsModel(BaseModel): @@ -101,10 +99,5 @@ class SampleModel(BaseModel): tumour: Annotated[str, BeforeValidator(get_boolean_as_string)] = NA_FIELD application: ApplicationModel methods: MethodsModel - metadata: Union[ - MipDNASampleMetadataModel, - BalsamicTargetedSampleMetadataModel, - BalsamicWGSSampleMetadataModel, - RnafusionSampleMetadataModel, - ] + metadata: MipDNASampleMetadataModel | BalsamicTargetedSampleMetadataModel | BalsamicWGSSampleMetadataModel | RnafusionSampleMetadataModel timestamps: TimestampModel diff --git a/cg/models/report/validators.py b/cg/models/report/validators.py index 93f443e2f5..f5e12c7d61 100644 --- a/cg/models/report/validators.py +++ b/cg/models/report/validators.py @@ -1,7 +1,7 @@ import logging from datetime import datetime from pathlib import Path -from typing import Any, Optional +from typing import Any from pydantic import ValidationInfo @@ -25,44 +25,44 @@ def get_report_string(value: Any) -> str: return str(value) if value else NA_FIELD -def get_boolean_as_string(value: Optional[bool]) -> str: +def get_boolean_as_string(value: bool | None) -> str: """Return delivery report adapted string representation of a boolean.""" if isinstance(value, bool): return YES_FIELD if value else NO_FIELD return NA_FIELD -def get_float_as_string(value: Optional[float]) -> str: +def get_float_as_string(value: float | None) -> str: """Return string representation of a float value.""" return str(round(float(value), PRECISION)) if value or isinstance(value, float) else NA_FIELD -def get_float_as_percentage(value: Optional[float]) -> str: +def get_float_as_percentage(value: float | None) -> str: """Return string percentage representation of a float value.""" return get_float_as_string(value * 100) if value or isinstance(value, float) else NA_FIELD -def get_date_as_string(date: Optional[datetime]) -> str: +def get_date_as_string(date: datetime | None) -> str: """Return the date string representation (year, month, day) of a datetime object.""" return str(date.date()) if date else NA_FIELD -def get_list_as_string(value: Optional[list[str]]) -> str: +def get_list_as_string(value: list[str] | None) -> str: """Return list elements as comma separated individual string values.""" return ", ".join(v for v in value) if value else NA_FIELD -def get_path_as_string(file_path: Optional[str]) -> str: +def get_path_as_string(file_path: str | None) -> str: """Return a report validated file name.""" return Path(file_path).name if file_path and Path(file_path).is_file() else NA_FIELD -def get_gender_as_string(gender: Optional[Gender]) -> str: +def get_gender_as_string(gender: Gender | None) -> str: """Return a report adapted gender.""" return get_report_string(REPORT_GENDER.get(gender)) -def get_prep_category_as_string(prep_category: Optional[PrepCategory]) -> str: +def get_prep_category_as_string(prep_category: PrepCategory | None) -> str: """Return a report validated prep category as string.""" if prep_category == OrderType.RML: LOG.error("The delivery report generation does not support RML samples") @@ -70,7 +70,7 @@ def get_prep_category_as_string(prep_category: Optional[PrepCategory]) -> str: return get_report_string(prep_category) -def get_analysis_type_as_string(analysis_type: Optional[str], info: ValidationInfo) -> str: +def get_analysis_type_as_string(analysis_type: str | None, info: ValidationInfo) -> str: """Return the analysis type as an accepted string value for the delivery report.""" if analysis_type and Pipeline.BALSAMIC in info.data.get("pipeline"): analysis_type: str = BALSAMIC_ANALYSIS_TYPE.get(analysis_type) diff --git a/cg/models/rnafusion/rnafusion.py b/cg/models/rnafusion/rnafusion.py index d88d742b5e..cbb1dff06f 100644 --- a/cg/models/rnafusion/rnafusion.py +++ b/cg/models/rnafusion/rnafusion.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional, Union from pydantic.v1 import BaseModel, Field @@ -11,19 +10,19 @@ class RnafusionQCMetrics(BaseModel): """RNAfusion QC metrics.""" - after_filtering_gc_content: Optional[float] - after_filtering_q20_rate: Optional[float] - after_filtering_q30_rate: Optional[float] - after_filtering_read1_mean_length: Optional[float] - before_filtering_total_reads: Optional[float] - bias_5_3: Optional[float] - pct_adapter: Optional[float] - pct_mrna_bases: Optional[float] - pct_ribosomal_bases: Optional[float] - pct_surviving: Optional[float] - pct_duplication: Optional[float] - reads_aligned: Optional[float] - uniquely_mapped_percent: Optional[float] + after_filtering_gc_content: float | None + after_filtering_q20_rate: float | None + after_filtering_q30_rate: float | None + after_filtering_read1_mean_length: float | None + before_filtering_total_reads: float | None + bias_5_3: float | None + pct_adapter: float | None + pct_mrna_bases: float | None + pct_ribosomal_bases: float | None + pct_surviving: float | None + pct_duplication: float | None + reads_aligned: float | None + uniquely_mapped_percent: float | None class RnafusionParameters(PipelineParameters): @@ -49,20 +48,20 @@ class RnafusionParameters(PipelineParameters): class CommandArgs(BaseModel): """Model for arguments and options supported.""" - log: Optional[Union[str, Path]] - resume: Optional[bool] - profile: Optional[str] - stub: Optional[bool] - config: Optional[Union[str, Path]] - name: Optional[str] - revision: Optional[str] - wait: Optional[str] - id: Optional[str] - with_tower: Optional[bool] - use_nextflow: Optional[bool] - compute_env: Optional[str] - work_dir: Optional[Union[str, Path]] - params_file: Optional[Union[str, Path]] + log: str | Path | None + resume: bool | None + profile: str | None + stub: bool | None + config: str | Path | None + name: str | None + revision: str | None + wait: str | None + id: str | None + with_tower: bool | None + use_nextflow: bool | None + compute_env: str | None + work_dir: str | Path | None + params_file: str | Path | None class RnafusionSampleSheetEntry(NextflowSampleSheetEntry): diff --git a/cg/models/scout/scout_load_config.py b/cg/models/scout/scout_load_config.py index acb8f82b3e..56cea847b0 100644 --- a/cg/models/scout/scout_load_config.py +++ b/cg/models/scout/scout_load_config.py @@ -1,7 +1,6 @@ """Class to hold information about scout load config""" from datetime import datetime -from typing import Optional from pydantic import BaseModel, BeforeValidator, ConfigDict from typing_extensions import Annotated, Literal @@ -10,21 +9,21 @@ class ChromographImages(BaseModel): - autozygous: Optional[str] = None - coverage: Optional[str] = None - upd_regions: Optional[str] = None - upd_sites: Optional[str] = None + autozygous: str | None = None + coverage: str | None = None + upd_regions: str | None = None + upd_sites: str | None = None class Reviewer(BaseModel): - alignment: Optional[str] = None - alignment_index: Optional[str] = None - vcf: Optional[str] = None - catalog: Optional[str] = None + alignment: str | None = None + alignment_index: str | None = None + vcf: str | None = None + catalog: str | None = None class ScoutIndividual(BaseModel): - alignment_path: Optional[str] = None + alignment_path: str | None = None analysis_type: Annotated[ Literal[ "external", @@ -38,75 +37,75 @@ class ScoutIndividual(BaseModel): ], BeforeValidator(field_not_none), ] = None - capture_kit: Optional[str] = None - confirmed_parent: Optional[bool] = None - confirmed_sex: Optional[bool] = None - father: Optional[str] = None - mother: Optional[str] = None - phenotype: Optional[str] = None + capture_kit: str | None = None + confirmed_parent: bool | None = None + confirmed_sex: bool | None = None + father: str | None = None + mother: str | None = None + phenotype: str | None = None sample_id: Annotated[str, BeforeValidator(field_not_none)] = None - sample_name: Optional[str] = None - sex: Annotated[Optional[str], BeforeValidator(field_not_none)] = None - subject_id: Optional[str] = None - tissue_type: Optional[str] = None + sample_name: str | None = None + sex: Annotated[str | None, BeforeValidator(field_not_none)] = None + subject_id: str | None = None + tissue_type: str | None = None model_config = ConfigDict(validate_assignment=True) class ScoutMipIndividual(ScoutIndividual): - mt_bam: Optional[str] = None + mt_bam: str | None = None chromograph_images: ChromographImages = ChromographImages() reviewer: Reviewer = Reviewer() - rhocall_bed: Optional[str] = None - rhocall_wig: Optional[str] = None - tiddit_coverage_wig: Optional[str] = None - upd_regions_bed: Optional[str] = None - upd_sites_bed: Optional[str] = None - vcf2cytosure: Optional[str] = None - mitodel_file: Optional[str] = None + rhocall_bed: str | None = None + rhocall_wig: str | None = None + tiddit_coverage_wig: str | None = None + upd_regions_bed: str | None = None + upd_sites_bed: str | None = None + vcf2cytosure: str | None = None + mitodel_file: str | None = None class ScoutCancerIndividual(ScoutIndividual): - tumor_type: Optional[str] = None - tmb: Optional[str] = None - msi: Optional[str] = None + tumor_type: str | None = None + tmb: str | None = None + msi: str | None = None tumor_purity: float = 0 - vcf2cytosure: Optional[str] = None + vcf2cytosure: str | None = None class ScoutLoadConfig(BaseModel): owner: Annotated[str, BeforeValidator(field_not_none)] = None family: Annotated[str, BeforeValidator(field_not_none)] = None - family_name: Optional[str] = None - synopsis: Optional[str] = None - phenotype_terms: Optional[list[str]] = None - phenotype_groups: Optional[list[str]] = None - gene_panels: Optional[list[str]] = None + family_name: str | None = None + synopsis: str | None = None + phenotype_terms: list[str] | None = None + phenotype_groups: list[str] | None = None + gene_panels: list[str] | None = None default_gene_panels: list[str] = [] - cohorts: Optional[list[str]] = None + cohorts: list[str] | None = None human_genome_build: str = None - rank_model_version: Optional[str] = None + rank_model_version: str | None = None rank_score_threshold: int = None - sv_rank_model_version: Optional[str] = None - analysis_date: Optional[datetime] = None + sv_rank_model_version: str | None = None + analysis_date: datetime | None = None samples: list[ScoutIndividual] = [] - delivery_report: Optional[str] = None - coverage_qc_report: Optional[str] = None - cnv_report: Optional[str] = None - multiqc: Optional[str] = None + delivery_report: str | None = None + coverage_qc_report: str | None = None + cnv_report: str | None = None + multiqc: str | None = None track: Literal["rare", "cancer"] = "rare" model_config = ConfigDict(validate_assignment=True) class BalsamicLoadConfig(ScoutLoadConfig): - madeline: Optional[str] = None + madeline: str | None = None vcf_cancer: Annotated[str, BeforeValidator(field_not_none)] = None - vcf_cancer_sv: Optional[str] = None - vcf_cancer_research: Optional[str] = None - vcf_cancer_sv_research: Optional[str] = None + vcf_cancer_sv: str | None = None + vcf_cancer_research: str | None = None + vcf_cancer_sv_research: str | None = None samples: list[ScoutCancerIndividual] = [] @@ -115,30 +114,30 @@ class BalsamicUmiLoadConfig(BalsamicLoadConfig): class MipLoadConfig(ScoutLoadConfig): - chromograph_image_files: Optional[list[str]] = None - chromograph_prefixes: Optional[list[str]] = None - madeline: Optional[str] = None - peddy_check: Optional[str] = None - peddy_ped: Optional[str] = None - peddy_sex: Optional[str] = None + chromograph_image_files: list[str] | None = None + chromograph_prefixes: list[str] | None = None + madeline: str | None = None + peddy_check: str | None = None + peddy_ped: str | None = None + peddy_sex: str | None = None samples: list[ScoutMipIndividual] = [] - smn_tsv: Optional[str] = None - variant_catalog: Optional[str] = None - vcf_mei: Optional[str] = None - vcf_mei_research: Optional[str] = None + smn_tsv: str | None = None + variant_catalog: str | None = None + vcf_mei: str | None = None + vcf_mei_research: str | None = None vcf_snv: Annotated[str, BeforeValidator(field_not_none)] = None - vcf_snv_research: Annotated[Optional[str], BeforeValidator(field_not_none)] = None - vcf_str: Optional[str] = None - vcf_sv: Annotated[Optional[str], BeforeValidator(field_not_none)] = None - vcf_sv_research: Annotated[Optional[str], BeforeValidator(field_not_none)] = None + vcf_snv_research: Annotated[str | None, BeforeValidator(field_not_none)] = None + vcf_str: str | None = None + vcf_sv: Annotated[str | None, BeforeValidator(field_not_none)] = None + vcf_sv_research: Annotated[str | None, BeforeValidator(field_not_none)] = None class RnafusionLoadConfig(ScoutLoadConfig): - multiqc_rna: Optional[str] = None - gene_fusion: Optional[str] = None - gene_fusion_report_research: Optional[str] = None - RNAfusion_inspector: Optional[str] = None - RNAfusion_inspector_research: Optional[str] = None - RNAfusion_report: Optional[str] = None - RNAfusion_report_research: Optional[str] = None + multiqc_rna: str | None = None + gene_fusion: str | None = None + gene_fusion_report_research: str | None = None + RNAfusion_inspector: str | None = None + RNAfusion_inspector_research: str | None = None + RNAfusion_report: str | None = None + RNAfusion_report_research: str | None = None samples: list[ScoutCancerIndividual] = [] diff --git a/cg/models/slurm/sbatch.py b/cg/models/slurm/sbatch.py index 0cd860491c..517973df2a 100644 --- a/cg/models/slurm/sbatch.py +++ b/cg/models/slurm/sbatch.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic import BaseModel from cg.constants.constants import HastaSlurmPartitions @@ -7,7 +5,7 @@ class Sbatch(BaseModel): - use_login_shell: Optional[str] = "" + use_login_shell: str | None = "" job_name: str account: str log_dir: str @@ -16,10 +14,10 @@ class Sbatch(BaseModel): minutes: str = "00" quality_of_service: SlurmQos = SlurmQos.LOW commands: str - error: Optional[str] = None - exclude: Optional[str] = "" - number_tasks: Optional[int] = None - memory: Optional[int] = None + error: str | None = None + exclude: str | None = "" + number_tasks: int | None = None + memory: int | None = None class SbatchDragen(Sbatch): diff --git a/cg/server/admin.py b/cg/server/admin.py index 4dfbbfe3bd..04c6e741a1 100644 --- a/cg/server/admin.py +++ b/cg/server/admin.py @@ -1,7 +1,6 @@ """Module for Flask-Admin views""" from datetime import datetime from gettext import gettext -from typing import Union from flask import flash, redirect, request, session, url_for from flask_admin.actions import action @@ -307,7 +306,7 @@ def action_set_hold(self, ids: list[str]): def action_set_empty(self, ids: list[str]): self.set_action_for_cases(action=None, case_entry_ids=ids) - def set_action_for_cases(self, action: Union[CaseActions, None], case_entry_ids: list[str]): + def set_action_for_cases(self, action: CaseActions | None, case_entry_ids: list[str]): try: for entry_id in case_entry_ids: case = db.get_case_by_entry_id(entry_id=entry_id) diff --git a/cg/server/api.py b/cg/server/api.py index dc64983304..d634132b34 100644 --- a/cg/server/api.py +++ b/cg/server/api.py @@ -4,7 +4,7 @@ import tempfile from functools import wraps from pathlib import Path -from typing import Any, Optional +from typing import Any import cachecontrol import requests @@ -31,8 +31,8 @@ Analysis, Application, ApplicationLimitations, - Customer, Case, + Customer, Flowcell, Pool, Sample, @@ -176,13 +176,13 @@ def get_cases(): return jsonify(families=cases_with_links, total=nr_cases) -def _get_current_customers() -> Optional[list[Customer]]: +def _get_current_customers() -> list[Customer] | None: """Return customers if the current user is not an admin.""" return g.current_user.customers if not g.current_user.is_admin else None def _get_cases( - enquiry: Optional[str], action: Optional[str], customers: Optional[list[Customer]] + enquiry: str | None, action: str | None, customers: list[Customer] | None ) -> list[Case]: """Get cases based on the provided filters.""" return db.get_cases_by_customers_action_and_case_search( @@ -247,7 +247,7 @@ def parse_samples(): elif request.args.get("status") == "sequencing": samples: list[Sample] = db.get_samples_to_sequence() else: - customers: Optional[list[Customer]] = ( + customers: list[Customer] | None = ( None if g.current_user.is_admin else g.current_user.customers ) samples: list[Sample] = db.get_samples_by_customer_id_and_pattern( @@ -298,7 +298,7 @@ def parse_sample_in_collaboration(sample_id): @BLUEPRINT.route("/pools") def parse_pools(): """Return pools.""" - customers: Optional[list[Customer]] = ( + customers: list[Customer] | None = ( g.current_user.customers if not g.current_user.is_admin else None ) pools: list[Pool] = db.get_pools_to_render( @@ -375,7 +375,7 @@ def parse_analyses(): @BLUEPRINT.route("/options") def parse_options(): """Return various options.""" - customers: list[Optional[Customer]] = ( + customers: list[Customer | None] = ( db.get_customers() if g.current_user.is_admin else g.current_user.customers ) diff --git a/cg/server/app.py b/cg/server/app.py index 1d3b6c6750..52bf27bb58 100644 --- a/cg/server/app.py +++ b/cg/server/app.py @@ -1,5 +1,3 @@ -from typing import Optional - import coloredlogs import requests from flask import Flask, redirect, session, url_for @@ -16,11 +14,11 @@ ApplicationVersion, Bed, BedVersion, + Case, + CaseSample, Collaboration, Customer, Delivery, - Case, - CaseSample, Flowcell, Invoice, Organism, @@ -140,6 +138,6 @@ def remove_database_session(exception=None): Remove the database session to ensure database resources are released when a request has been processed. """ - scoped_session_registry: Optional[scoped_session] = get_scoped_session_registry() + scoped_session_registry: scoped_session | None = get_scoped_session_registry() if scoped_session_registry: scoped_session_registry.remove() diff --git a/cg/server/invoices/views.py b/cg/server/invoices/views.py index 6853a2d6c1..b587ee04b5 100644 --- a/cg/server/invoices/views.py +++ b/cg/server/invoices/views.py @@ -2,7 +2,6 @@ import os import tempfile from datetime import date -from typing import Union from flask import ( Blueprint, @@ -41,7 +40,7 @@ def logged_in(): def undo_invoice(invoice_id): invoice_obj: Invoice = db.get_invoice_by_entry_id(entry_id=invoice_id) record_type: str = invoice_obj.record_type - records: list[Union[Pool, Sample]] = db.get_pools_and_samples_for_invoice_by_invoice_id( + records: list[Pool | Sample] = db.get_pools_and_samples_for_invoice_by_invoice_id( invoice_id=invoice_id ) db.session.delete(invoice_obj) @@ -140,14 +139,12 @@ def new(record_type): customer_id = request.args.get("customer", "cust002") customer: Customer = db.get_customer_by_internal_id(customer_internal_id=customer_id) if record_type == "Sample": - records: list[Union[Pool, Sample]] = db.get_samples_to_invoice_for_customer( - customer=customer - ) + records: list[Pool | Sample] = db.get_samples_to_invoice_for_customer(customer=customer) customers_to_invoice: list[Customer] = db.get_customers_to_invoice( records=db.get_samples_to_invoice_query() ) elif record_type == "Pool": - records: list[Union[Pool, Sample]] = db.get_pools_to_invoice_for_customer(customer=customer) + records: list[Pool | Sample] = db.get_pools_to_invoice_for_customer(customer=customer) customers_to_invoice: list[Customer] = db.get_customers_to_invoice( records=db.get_pools_to_invoice_query() ) diff --git a/cg/store/api/add.py b/cg/store/api/add.py index 61497b771b..0cf75f14a9 100644 --- a/cg/store/api/add.py +++ b/cg/store/api/add.py @@ -1,6 +1,5 @@ import logging from datetime import datetime -from typing import Optional import petname @@ -13,11 +12,11 @@ ApplicationVersion, Bed, BedVersion, + Case, + CaseSample, Collaboration, Customer, Delivery, - Case, - CaseSample, Flowcell, Invoice, Organism, @@ -197,10 +196,10 @@ def add_case( data_delivery: DataDelivery, name: str, ticket: str, - panels: Optional[list[str]] = None, - cohorts: Optional[list[str]] = None, - priority: Optional[Priority] = Priority.standard, - synopsis: Optional[str] = None, + panels: list[str] | None = None, + cohorts: list[str] | None = None, + priority: Priority | None = Priority.standard, + synopsis: str | None = None, ) -> Case: """Build a new Case record.""" @@ -247,8 +246,8 @@ def add_flow_cell( sequencer_name: str, sequencer_type: str, date: datetime, - flow_cell_status: Optional[str] = FlowCellStatus.ON_DISK, - has_backup: Optional[bool] = False, + flow_cell_status: str | None = FlowCellStatus.ON_DISK, + has_backup: bool | None = False, ) -> Flowcell: """Build a new Flowcell record.""" return Flowcell( @@ -354,7 +353,7 @@ def add_invoice( comment: str = None, discount: int = 0, record_type: str = None, - invoiced_at: Optional[datetime] = None, + invoiced_at: datetime | None = None, ): """Build a new Invoice record.""" diff --git a/cg/store/api/base.py b/cg/store/api/base.py index 73e9b107d4..ce11e2de69 100644 --- a/cg/store/api/base.py +++ b/cg/store/api/base.py @@ -1,21 +1,24 @@ """All models aggregated in a base class.""" from dataclasses import dataclass -from typing import Callable, Optional, Type +from typing import Callable, Type from sqlalchemy import and_, func from sqlalchemy.orm import Query, Session from cg.store.filters.status_case_filters import CaseFilter, apply_case_filter -from cg.store.filters.status_customer_filters import CustomerFilter, apply_customer_filter +from cg.store.filters.status_customer_filters import ( + CustomerFilter, + apply_customer_filter, +) from cg.store.filters.status_sample_filters import SampleFilter, apply_sample_filter from cg.store.models import ( Analysis, Application, ApplicationLimitations, ApplicationVersion, - Customer, Case, CaseSample, + Customer, Flowcell, ) from cg.store.models import Model as ModelBase @@ -111,7 +114,7 @@ def _get_latest_analyses_for_cases_query(self) -> Query: def _get_filtered_case_query( self, - case_action: Optional[str], + case_action: str | None, customer_id: str, data_analysis: str, days: int, diff --git a/cg/store/api/find_basic_data.py b/cg/store/api/find_basic_data.py index e1322c9a77..a9bfe59f32 100644 --- a/cg/store/api/find_basic_data.py +++ b/cg/store/api/find_basic_data.py @@ -1,6 +1,5 @@ """Handler to find basic data objects""" import datetime as dt -from typing import Optional from sqlalchemy.orm import Query, Session @@ -78,7 +77,7 @@ def get_applications(self) -> list[Application]: .all() ) - def get_current_application_version_by_tag(self, tag: str) -> Optional[ApplicationVersion]: + def get_current_application_version_by_tag(self, tag: str) -> ApplicationVersion | None: """Return the current application version for an application tag.""" application = self.get_application_by_tag(tag=tag) if not application: diff --git a/cg/store/api/find_business_data.py b/cg/store/api/find_business_data.py index 027abbfd0d..a9279a1e26 100644 --- a/cg/store/api/find_business_data.py +++ b/cg/store/api/find_business_data.py @@ -1,7 +1,7 @@ """Handler to find business data objects.""" import datetime as dt import logging -from typing import Callable, Iterator, Optional, Union +from typing import Callable, Iterator from sqlalchemy.orm import Query, Session @@ -10,26 +10,41 @@ from cg.constants.indexes import ListIndexes from cg.exc import CaseNotFoundError, CgError from cg.store.api.base import BaseHandler -from cg.store.filters.status_analysis_filters import AnalysisFilter, apply_analysis_filter +from cg.store.filters.status_analysis_filters import ( + AnalysisFilter, + apply_analysis_filter, +) from cg.store.filters.status_application_limitations_filters import ( ApplicationLimitationsFilter, apply_application_limitations_filter, ) from cg.store.filters.status_case_filters import CaseFilter, apply_case_filter -from cg.store.filters.status_case_sample_filters import CaseSampleFilter, apply_case_sample_filter -from cg.store.filters.status_customer_filters import CustomerFilter, apply_customer_filter -from cg.store.filters.status_flow_cell_filters import FlowCellFilter, apply_flow_cell_filter +from cg.store.filters.status_case_sample_filters import ( + CaseSampleFilter, + apply_case_sample_filter, +) +from cg.store.filters.status_customer_filters import ( + CustomerFilter, + apply_customer_filter, +) +from cg.store.filters.status_flow_cell_filters import ( + FlowCellFilter, + apply_flow_cell_filter, +) from cg.store.filters.status_invoice_filters import InvoiceFilter, apply_invoice_filter -from cg.store.filters.status_metrics_filters import SequencingMetricsFilter, apply_metrics_filter +from cg.store.filters.status_metrics_filters import ( + SequencingMetricsFilter, + apply_metrics_filter, +) from cg.store.filters.status_pool_filters import PoolFilter, apply_pool_filter from cg.store.filters.status_sample_filters import SampleFilter, apply_sample_filter from cg.store.models import ( Analysis, Application, ApplicationLimitations, - Customer, Case, CaseSample, + Customer, Flowcell, Invoice, Pool, @@ -153,19 +168,19 @@ def get_cases_by_customer_and_case_name_search( def get_cases_by_customers_action_and_case_search( self, - customers: Optional[list[Customer]], - action: Optional[str], - case_search: Optional[str], - limit: Optional[int] = 30, + customers: list[Customer] | None, + action: str | None, + case_search: str | None, + limit: int | None = 30, ) -> list[Case]: """ Retrieve a list of cases filtered by customers, action, and matching names or internal ids. Args: - customers (Optional[list[Customer]]): A list of customer objects to filter cases by. - action (Optional[str]): The action string to filter cases by. - case_search (Optional[str]): The case search string to filter cases by. - limit (Optional[int], default=30): The maximum number of cases to return. + customers (list[Customer] | None): A list of customer objects to filter cases by. + action (str | None): The action string to filter cases by. + case_search (str | None): The case search string to filter cases by. + limit (int | None, default=30): The maximum number of cases to return. Returns: list[Case]: A list of filtered cases sorted by creation time and limited by the specified number. @@ -192,19 +207,19 @@ def get_cases_by_customers_action_and_case_search( def get_cases_by_customer_pipeline_and_case_search( self, - customer: Optional[Customer], - pipeline: Optional[str], - case_search: Optional[str], - limit: Optional[int] = 30, + customer: Customer | None, + pipeline: str | None, + case_search: str | None, + limit: int | None = 30, ) -> list[Case]: """ Retrieve a list of cases filtered by customer, pipeline, and matching names or internal ids. Args: - customer (Optional[Customer]): A customer object to filter cases by. - pipeline (Optional[str]): The pipeline string to filter cases by. - case_search (Optional[str]): The case search string to filter cases by. - limit (Optional[int], default=30): The maximum number of cases to return. + customer (Customer | None): A customer object to filter cases by. + pipeline (str | None): The pipeline string to filter cases by. + case_search (str | None): The case search string to filter cases by. + limit (int | None, default=30): The maximum number of cases to return. Returns: list[Case]: A list of filtered cases sorted by creation time and limited by the specified number. @@ -349,7 +364,7 @@ def get_number_of_reads_for_sample_passing_q30_threshold( sample_internal_id=sample_internal_id, q30_threshold=q30_threshold, ) - reads_count: Optional[int] = total_reads_query.scalar() + reads_count: int | None = total_reads_query.scalar() return reads_count if reads_count else 0 def get_average_q30_for_sample_on_flow_cell( @@ -425,7 +440,7 @@ def get_metrics_entry_by_flow_cell_name_sample_internal_id_and_lane( lane=lane, ).first() - def get_flow_cell_by_name(self, flow_cell_name: str) -> Optional[Flowcell]: + def get_flow_cell_by_name(self, flow_cell_name: str) -> Flowcell | None: """Return flow cell by flow cell name.""" return apply_flow_cell_filter( flow_cells=self._get_query(table=Flowcell), @@ -433,7 +448,7 @@ def get_flow_cell_by_name(self, flow_cell_name: str) -> Optional[Flowcell]: filter_functions=[FlowCellFilter.FILTER_BY_NAME], ).first() - def get_flow_cells_by_statuses(self, flow_cell_statuses: list[str]) -> Optional[list[Flowcell]]: + def get_flow_cells_by_statuses(self, flow_cell_statuses: list[str]) -> list[Flowcell] | None: """Return flow cells with supplied statuses.""" return apply_flow_cell_filter( flow_cells=self._get_query(table=Flowcell), @@ -456,7 +471,7 @@ def get_flow_cell_by_name_pattern_and_status( filter_functions=filter_functions, ).all() - def get_flow_cells_by_case(self, case: Case) -> Optional[list[Flowcell]]: + def get_flow_cells_by_case(self, case: Case) -> list[Flowcell] | None: """Return flow cells for case.""" return apply_flow_cell_filter( flow_cells=self._get_join_flow_cell_sample_links_query(), @@ -464,7 +479,7 @@ def get_flow_cells_by_case(self, case: Case) -> Optional[list[Flowcell]]: case=case, ).all() - def get_samples_from_flow_cell(self, flow_cell_id: str) -> Optional[list[Sample]]: + def get_samples_from_flow_cell(self, flow_cell_id: str) -> list[Sample] | None: """Return samples present on flow cell.""" flow_cell: Flowcell = self.get_flow_cell_by_name(flow_cell_name=flow_cell_id) if flow_cell: @@ -472,7 +487,7 @@ def get_samples_from_flow_cell(self, flow_cell_id: str) -> Optional[list[Sample] def are_all_flow_cells_on_disk(self, case_id: str) -> bool: """Check if flow cells are on disk for sample before starting the analysis.""" - flow_cells: Optional[list[Flowcell]] = self.get_flow_cells_by_case( + flow_cells: list[Flowcell] | None = self.get_flow_cells_by_case( case=self.get_case_by_internal_id(internal_id=case_id) ) if not flow_cells: @@ -482,7 +497,7 @@ def are_all_flow_cells_on_disk(self, case_id: str) -> bool: def request_flow_cells_for_case(self, case_id) -> None: """Set the status of removed flow cells to REQUESTED for the given case.""" - flow_cells: Optional[list[Flowcell]] = self.get_flow_cells_by_case( + flow_cells: list[Flowcell] | None = self.get_flow_cells_by_case( case=self.get_case_by_internal_id(internal_id=case_id) ) for flow_cell in flow_cells: @@ -514,7 +529,7 @@ def get_invoice_by_entry_id(self, entry_id: int) -> Invoice: def get_pools_and_samples_for_invoice_by_invoice_id( self, *, invoice_id: int = None - ) -> list[Union[Pool, Sample]]: + ) -> list[Pool | Sample]: """Return all pools and samples for an invoice.""" pools: list[Pool] = apply_pool_filter( pools=self._get_query(table=Pool), @@ -547,7 +562,7 @@ def new_invoice_id(self) -> int: ids = [inv.id for inv in query] return max(ids) + 1 if ids else 0 - def get_pools_by_customer_id(self, *, customers: Optional[list[Customer]] = None) -> list[Pool]: + def get_pools_by_customer_id(self, *, customers: list[Customer] | None = None) -> list[Pool]: """Return all the pools for a customer.""" customer_ids = [customer.id for customer in customers] return apply_pool_filter( @@ -584,7 +599,7 @@ def get_pool_by_entry_id(self, entry_id: int) -> Pool: ).first() def get_pools_to_render( - self, customers: Optional[list[Customer]] = None, enquiry: str = None + self, customers: list[Customer] | None = None, enquiry: str = None ) -> list[Pool]: pools: list[Pool] = ( self.get_pools_by_customer_id(customers=customers) if customers else self.get_pools() @@ -611,7 +626,7 @@ def get_ready_made_library_expected_reads(self, case_id: str) -> int: return application.expected_reads def get_samples_by_customer_id_and_pattern( - self, *, customers: Optional[list[Customer]] = None, pattern: str = None + self, *, customers: list[Customer] | None = None, pattern: str = None ) -> list[Sample]: """Get samples by customer and sample internal id or sample name pattern.""" samples: Query = self._get_query(table=Sample) @@ -693,7 +708,7 @@ def get_sample_by_name(self, name: str) -> Sample: samples=samples, filter_functions=[SampleFilter.FILTER_BY_SAMPLE_NAME], name=name ).first() - def get_samples_by_type(self, case_id: str, sample_type: SampleType) -> Optional[list[Sample]]: + def get_samples_by_type(self, case_id: str, sample_type: SampleType) -> list[Sample] | None: """Get samples given a tissue type.""" samples: Query = apply_case_sample_filter( filter_functions=[CaseSampleFilter.GET_SAMPLES_IN_CASE_BY_INTERNAL_ID], diff --git a/cg/store/api/status.py b/cg/store/api/status.py index 0ce0da02de..786ab8fb46 100644 --- a/cg/store/api/status.py +++ b/cg/store/api/status.py @@ -1,6 +1,5 @@ from datetime import datetime, timedelta from types import SimpleNamespace -from typing import Optional from sqlalchemy.orm import Query, Session from typing_extensions import Literal @@ -9,13 +8,22 @@ from cg.constants.constants import CaseActions from cg.constants.invoice import CustomerNames from cg.store.api.base import BaseHandler -from cg.store.filters.status_analysis_filters import AnalysisFilter, apply_analysis_filter -from cg.store.filters.status_application_filters import ApplicationFilter, apply_application_filter +from cg.store.filters.status_analysis_filters import ( + AnalysisFilter, + apply_analysis_filter, +) +from cg.store.filters.status_application_filters import ( + ApplicationFilter, + apply_application_filter, +) from cg.store.filters.status_case_filters import CaseFilter, apply_case_filter -from cg.store.filters.status_flow_cell_filters import FlowCellFilter, apply_flow_cell_filter +from cg.store.filters.status_flow_cell_filters import ( + FlowCellFilter, + apply_flow_cell_filter, +) from cg.store.filters.status_pool_filters import PoolFilter, apply_pool_filter from cg.store.filters.status_sample_filters import SampleFilter, apply_sample_filter -from cg.store.models import Analysis, Customer, Case, Flowcell, Pool, Sample +from cg.store.models import Analysis, Case, Customer, Flowcell, Pool, Sample class StatusHandler(BaseHandler): @@ -123,7 +131,7 @@ def cases( internal_id: str = None, name: str = None, days: int = 0, - case_action: Optional[str] = None, + case_action: str | None = None, priority: str = None, customer_id: str = None, exclude_customer_id: str = None, @@ -236,7 +244,7 @@ def get_sample_by_entry_id(self, entry_id: int) -> Sample: entry_id=entry_id, ).first() - def get_sample_by_internal_id(self, internal_id: str) -> Optional[Sample]: + def get_sample_by_internal_id(self, internal_id: str) -> Sample | None: """Return a sample by lims id.""" return apply_sample_filter( filter_functions=[SampleFilter.FILTER_BY_INTERNAL_ID], diff --git a/cg/store/database.py b/cg/store/database.py index 176b543fd7..a0930d3114 100644 --- a/cg/store/database.py +++ b/cg/store/database.py @@ -1,5 +1,3 @@ -from typing import List, Optional - from sqlalchemy import create_engine, inspect from sqlalchemy.engine.base import Engine from sqlalchemy.engine.reflection import Inspector @@ -8,8 +6,8 @@ from cg.exc import CgError from cg.store.models import Model -SESSION: Optional[scoped_session] = None -ENGINE: Optional[Engine] = None +SESSION: scoped_session | None = None +ENGINE: Engine | None = None def initialize_database(db_uri: str) -> None: @@ -27,7 +25,7 @@ def get_session() -> Session: return SESSION -def get_scoped_session_registry() -> Optional[scoped_session]: +def get_scoped_session_registry() -> scoped_session | None: """Get the scoped session registry for status db.""" return SESSION @@ -51,7 +49,7 @@ def drop_all_tables() -> None: Model.metadata.drop_all(bind=session.get_bind()) -def get_tables() -> List[str]: +def get_tables() -> list[str]: """Get a list of all tables in status db.""" engine: Engine = get_engine() inspector: Inspector = inspect(engine) diff --git a/cg/store/filters/status_bed_filters.py b/cg/store/filters/status_bed_filters.py index c1497a2993..c33eba0cf9 100644 --- a/cg/store/filters/status_bed_filters.py +++ b/cg/store/filters/status_bed_filters.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Optional +from typing import Callable from sqlalchemy.orm import Query @@ -38,8 +38,8 @@ class BedFilter(Enum): def apply_bed_filter( beds: Query, filter_functions: list[Callable], - bed_entry_id: Optional[int] = None, - bed_name: Optional[str] = None, + bed_entry_id: int | None = None, + bed_name: str | None = None, ) -> Query: """Apply filtering functions and return filtered results.""" for function in filter_functions: diff --git a/cg/store/filters/status_bed_version_filters.py b/cg/store/filters/status_bed_version_filters.py index 10b90211ae..070c8ebbc5 100644 --- a/cg/store/filters/status_bed_version_filters.py +++ b/cg/store/filters/status_bed_version_filters.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Optional +from typing import Callable from sqlalchemy.orm import Query @@ -30,8 +30,8 @@ class BedVersionFilter(Enum): def apply_bed_version_filter( bed_versions: Query, filter_functions: list[Callable], - bed_version_file_name: Optional[str] = None, - bed_version_short_name: Optional[str] = None, + bed_version_file_name: str | None = None, + bed_version_short_name: str | None = None, ) -> Query: """Apply filtering functions and return filtered results.""" for function in filter_functions: diff --git a/cg/store/filters/status_case_filters.py b/cg/store/filters/status_case_filters.py index 222e54d38a..da020aa42f 100644 --- a/cg/store/filters/status_case_filters.py +++ b/cg/store/filters/status_case_filters.py @@ -1,6 +1,6 @@ from datetime import datetime from enum import Enum -from typing import Callable, Optional +from typing import Callable from sqlalchemy import and_, not_, or_ from sqlalchemy.orm import Query @@ -12,7 +12,7 @@ LOQUSDB_MIP_SEQUENCING_METHODS, LOQUSDB_SUPPORTED_PIPELINES, ) -from cg.store.models import Analysis, Application, Customer, Case, Sample +from cg.store.models import Analysis, Application, Case, Customer, Sample def filter_cases_by_action(cases: Query, action: str, **kwargs) -> Query: @@ -203,21 +203,21 @@ def order_cases_by_created_at(cases: Query, **kwargs) -> Query: def apply_case_filter( cases: Query, filter_functions: list[Callable], - action: Optional[str] = None, - case_search: Optional[str] = None, - creation_date: Optional[datetime] = None, - customer_entry_id: Optional[int] = None, - customer_entry_ids: Optional[list[int]] = None, - entry_id: Optional[int] = None, - internal_id: Optional[str] = None, - internal_id_search: Optional[str] = None, - name: Optional[str] = None, - name_search: Optional[str] = None, - order_date: Optional[datetime] = None, - pipeline: Optional[Pipeline] = None, - pipeline_search: Optional[str] = None, - priority: Optional[str] = None, - ticket_id: Optional[str] = None, + action: str | None = None, + case_search: str | None = None, + creation_date: datetime | None = None, + customer_entry_id: int | None = None, + customer_entry_ids: list[int] | None = None, + entry_id: int | None = None, + internal_id: str | None = None, + internal_id_search: str | None = None, + name: str | None = None, + name_search: str | None = None, + order_date: datetime | None = None, + pipeline: Pipeline | None = None, + pipeline_search: str | None = None, + priority: str | None = None, + ticket_id: str | None = None, ) -> Query: """Apply filtering functions and return filtered results.""" for function in filter_functions: diff --git a/cg/store/filters/status_case_sample_filters.py b/cg/store/filters/status_case_sample_filters.py index 6946e604bf..55ee595de4 100644 --- a/cg/store/filters/status_case_sample_filters.py +++ b/cg/store/filters/status_case_sample_filters.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Optional +from typing import Callable from sqlalchemy.orm import Query @@ -21,9 +21,9 @@ def get_cases_with_sample_by_internal_id(case_samples: Query, sample_internal_id def apply_case_sample_filter( filter_functions: list[Callable], case_samples: Query, - case_internal_id: Optional[str] = None, - sample_entry_id: Optional[int] = None, - sample_internal_id: Optional[str] = None, + case_internal_id: str | None = None, + sample_entry_id: int | None = None, + sample_internal_id: str | None = None, ) -> Query: """Apply filtering functions to the sample queries and return filtered results.""" diff --git a/cg/store/filters/status_collaboration_filters.py b/cg/store/filters/status_collaboration_filters.py index c87c28be0d..74324f79b7 100644 --- a/cg/store/filters/status_collaboration_filters.py +++ b/cg/store/filters/status_collaboration_filters.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Optional +from typing import Callable from sqlalchemy.orm import Query @@ -18,7 +18,7 @@ class CollaborationFilter(Enum): def apply_collaboration_filter( - collaborations: Query, filter_functions: list[Callable], internal_id: Optional[str] = None + collaborations: Query, filter_functions: list[Callable], internal_id: str | None = None ) -> Query: """Apply filtering functions and return filtered results.""" for function in filter_functions: diff --git a/cg/store/filters/status_customer_filters.py b/cg/store/filters/status_customer_filters.py index 5109b0a18f..054994654d 100644 --- a/cg/store/filters/status_customer_filters.py +++ b/cg/store/filters/status_customer_filters.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Optional +from typing import Callable from sqlalchemy.orm import Query @@ -30,8 +30,8 @@ class CustomerFilter(Enum): def apply_customer_filter( customers: Query, filter_functions: list[Callable], - customer_internal_id: Optional[str] = None, - exclude_customer_internal_id: Optional[str] = None, + customer_internal_id: str | None = None, + exclude_customer_internal_id: str | None = None, ) -> Query: """Apply filtering functions and return filtered results.""" for filter_function in filter_functions: diff --git a/cg/store/filters/status_flow_cell_filters.py b/cg/store/filters/status_flow_cell_filters.py index d2a97fb4f9..409cd4d333 100644 --- a/cg/store/filters/status_flow_cell_filters.py +++ b/cg/store/filters/status_flow_cell_filters.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Optional +from typing import Callable from sqlalchemy.orm import Query @@ -31,10 +31,10 @@ def filter_flow_cells_with_statuses( def apply_flow_cell_filter( flow_cells: Query, filter_functions: list[Callable], - case: Optional[Case] = None, - flow_cell_name: Optional[str] = None, - name_search: Optional[str] = None, - flow_cell_statuses: Optional[list[str]] = None, + case: Case | None = None, + flow_cell_name: str | None = None, + name_search: str | None = None, + flow_cell_statuses: list[str] | None = None, ) -> Query: """Apply filtering functions and return filtered results.""" for function in filter_functions: diff --git a/cg/store/filters/status_invoice_filters.py b/cg/store/filters/status_invoice_filters.py index ea6a81497f..e7a0fd90f1 100644 --- a/cg/store/filters/status_invoice_filters.py +++ b/cg/store/filters/status_invoice_filters.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Optional +from typing import Callable from sqlalchemy.orm import Query @@ -24,7 +24,7 @@ def filter_invoices_not_invoiced(invoices: Query, **kwargs) -> Query: def apply_invoice_filter( filter_functions: list[str], invoices: Query, - entry_id: Optional[int] = None, + entry_id: int | None = None, ) -> Query: """Apply filtering functions to the invoice queries and return filtered results.""" diff --git a/cg/store/filters/status_metrics_filters.py b/cg/store/filters/status_metrics_filters.py index 41294da91c..c718f959e0 100644 --- a/cg/store/filters/status_metrics_filters.py +++ b/cg/store/filters/status_metrics_filters.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Optional +from typing import Callable from sqlalchemy import func from sqlalchemy.orm import Query @@ -60,10 +60,10 @@ class SequencingMetricsFilter(Enum): def apply_metrics_filter( metrics: Query, filter_functions: list[Callable], - sample_internal_id: Optional[str] = None, - flow_cell_name: Optional[str] = None, - lane: Optional[int] = None, - q30_threshold: Optional[int] = None, + sample_internal_id: str | None = None, + flow_cell_name: str | None = None, + lane: int | None = None, + q30_threshold: int | None = None, ) -> Query: for filter_function in filter_functions: metrics: Query = filter_function( diff --git a/cg/store/filters/status_organism_filters.py b/cg/store/filters/status_organism_filters.py index 876a0e38b9..5f70108a39 100644 --- a/cg/store/filters/status_organism_filters.py +++ b/cg/store/filters/status_organism_filters.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Optional +from typing import Callable from sqlalchemy.orm import Query @@ -20,7 +20,7 @@ class OrganismFilter(Enum): def apply_organism_filter( organisms: Query, filter_functions: list[Callable], - internal_id: Optional[str] = None, + internal_id: str | None = None, ) -> Query: """Apply filtering functions and return filtered results.""" for filter_function in filter_functions: diff --git a/cg/store/filters/status_panel_filters.py b/cg/store/filters/status_panel_filters.py index 1f0948ae37..b44b228c0a 100644 --- a/cg/store/filters/status_panel_filters.py +++ b/cg/store/filters/status_panel_filters.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Optional +from typing import Callable from sqlalchemy.orm import Query @@ -20,7 +20,7 @@ class PanelFilter(Enum): def apply_panel_filter( panels: Query, filters: list[PanelFilter], - abbreviation: Optional[str] = None, + abbreviation: str | None = None, ) -> Query: """Apply filtering functions and return filtered results.""" for filter_function in filters: diff --git a/cg/store/filters/status_pool_filters.py b/cg/store/filters/status_pool_filters.py index f4abc66939..795a352f55 100644 --- a/cg/store/filters/status_pool_filters.py +++ b/cg/store/filters/status_pool_filters.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Optional +from typing import Callable from sqlalchemy.orm import Query @@ -74,13 +74,13 @@ def filter_pools_by_customer(pools: Query, customer: Customer, **kwargs) -> Quer def apply_pool_filter( filter_functions: list[Callable], pools: Query, - invoice_id: Optional[int] = None, - entry_id: Optional[int] = None, - name: Optional[str] = None, - customer_ids: Optional[list[int]] = None, - name_enquiry: Optional[str] = None, - order_enquiry: Optional[str] = None, - customer: Optional[Customer] = None, + invoice_id: int | None = None, + entry_id: int | None = None, + name: str | None = None, + customer_ids: list[int] | None = None, + name_enquiry: str | None = None, + order_enquiry: str | None = None, + customer: Customer | None = None, ) -> Query: """Apply filtering functions to the pool queries and return filtered results""" diff --git a/cg/store/filters/status_sample_filters.py b/cg/store/filters/status_sample_filters.py index f23f6dd43e..8d58960f62 100644 --- a/cg/store/filters/status_sample_filters.py +++ b/cg/store/filters/status_sample_filters.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Any, Callable, Optional +from typing import Any, Callable from sqlalchemy import or_ from sqlalchemy.orm import Query @@ -160,18 +160,18 @@ def filter_samples_by_identifier_name_and_value( def apply_sample_filter( filter_functions: list[Callable], samples: Query, - entry_id: Optional[int] = None, - internal_id: Optional[str] = None, - tissue_type: Optional[SampleType] = None, - data_analysis: Optional[str] = None, - invoice_id: Optional[int] = None, - customer_entry_ids: Optional[list[int]] = None, - subject_id: Optional[str] = None, - name: Optional[str] = None, - customer: Optional[Customer] = None, - name_pattern: Optional[str] = None, - internal_id_pattern: Optional[str] = None, - search_pattern: Optional[str] = None, + entry_id: int | None = None, + internal_id: str | None = None, + tissue_type: SampleType | None = None, + data_analysis: str | None = None, + invoice_id: int | None = None, + customer_entry_ids: list[int] | None = None, + subject_id: str | None = None, + name: str | None = None, + customer: Customer | None = None, + name_pattern: str | None = None, + internal_id_pattern: str | None = None, + search_pattern: str | None = None, identifier_name: str = None, identifier_value: Any = None, ) -> Query: diff --git a/cg/store/filters/status_user_filters.py b/cg/store/filters/status_user_filters.py index 1d0a1c18ef..c5d1bdcd97 100644 --- a/cg/store/filters/status_user_filters.py +++ b/cg/store/filters/status_user_filters.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Callable, Optional +from typing import Callable from sqlalchemy.orm import Query @@ -20,7 +20,7 @@ class UserFilter(Enum): def apply_user_filter( users: Query, filter_functions: list[Callable], - email: Optional[str] = None, + email: str | None = None, ) -> Query: """Apply filtering functions and return filtered results.""" for filter_function in filter_functions: diff --git a/cg/store/models.py b/cg/store/models.py index 137ff93681..67cb3a6f27 100644 --- a/cg/store/models.py +++ b/cg/store/models.py @@ -1,6 +1,5 @@ import datetime as dt import re -from typing import Optional from sqlalchemy import Column, ForeignKey, Table, UniqueConstraint, orm, types from sqlalchemy.orm import declarative_base @@ -421,16 +420,16 @@ def panels(self, panel_list: list[str]): self._panels = ",".join(panel_list) if panel_list else None @property - def latest_ticket(self) -> Optional[str]: + def latest_ticket(self) -> str | None: """Returns the last ticket the family was ordered in""" return self.tickets.split(sep=",")[-1] if self.tickets else None @property - def latest_analyzed(self) -> Optional[dt.datetime]: + def latest_analyzed(self) -> dt.datetime | None: return self.analyses[0].completed_at if self.analyses else None @property - def latest_sequenced(self) -> Optional[dt.datetime]: + def latest_sequenced(self) -> dt.datetime | None: sequenced_dates = [] for link in self.links: if link.sample.application_version.application.is_external: diff --git a/cg/utils/date.py b/cg/utils/date.py index 7a46920252..0db5810acb 100644 --- a/cg/utils/date.py +++ b/cg/utils/date.py @@ -1,9 +1,7 @@ """Module to parse dates.""" -import datetime import logging import re from datetime import datetime, timedelta -from typing import Optional from cg.constants.symbols import DASH, DOT, FWD_SLASH, SPACE @@ -20,7 +18,7 @@ def match_date(date: str) -> bool: return bool(re.match(date_pattern, date)) -def get_date(date: Optional[str] = None, date_format: Optional[str] = None) -> datetime: +def get_date(date: str | None = None, date_format: str | None = None) -> datetime: """Return a datetime object if there is a valid date. Raise exception if date is not valid. diff --git a/cg/utils/utils.py b/cg/utils/utils.py index 5f5f322eca..1643fd3472 100644 --- a/cg/utils/utils.py +++ b/cg/utils/utils.py @@ -1,9 +1,7 @@ """Helper functions.""" -from typing import Optional - -def get_string_from_list_by_pattern(strings: list[str], pattern: str) -> Optional[str]: +def get_string_from_list_by_pattern(strings: list[str], pattern: str) -> str | None: """Returns the full string from a list given a specific pattern.""" return next((string for string in strings if pattern in string), None) if strings else None diff --git a/tests/apps/madeline/conftest.py b/tests/apps/madeline/conftest.py index 089e1cdb27..b104c89d94 100644 --- a/tests/apps/madeline/conftest.py +++ b/tests/apps/madeline/conftest.py @@ -1,7 +1,6 @@ """Fixtures for testing the madeline cg app""" from pathlib import Path -from typing import Optional import pytest @@ -32,7 +31,7 @@ def madeline_columns() -> dict[str, str]: @pytest.fixture -def mother() -> dict[str, Optional[str]]: +def mother() -> dict[str, str | None]: """return a dictionary with ind info""" ind_info = { "sample": "mother", @@ -44,7 +43,7 @@ def mother() -> dict[str, Optional[str]]: @pytest.fixture -def father() -> dict[str, Optional[str]]: +def father() -> dict[str, str | None]: """return a dictionary with ind info""" ind_info = { "sample": "father", @@ -56,7 +55,7 @@ def father() -> dict[str, Optional[str]]: @pytest.fixture -def proband() -> dict[str, Optional[str]]: +def proband() -> dict[str, str | None]: """return a dictionary with ind info""" ind_info = { "sample": "proband", @@ -71,7 +70,7 @@ def proband() -> dict[str, Optional[str]]: @pytest.fixture -def trio(proband: dict, mother: dict, father: dict) -> list[dict[str, Optional[str]]]: +def trio(proband: dict, mother: dict, father: dict) -> list[dict[str, str | None]]: """return a list with a trio""" return [proband, mother, father] diff --git a/tests/apps/orderform/test_excel_orderform_parser.py b/tests/apps/orderform/test_excel_orderform_parser.py index 8ac3228af8..3e6af3aa64 100644 --- a/tests/apps/orderform/test_excel_orderform_parser.py +++ b/tests/apps/orderform/test_excel_orderform_parser.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import Optional from cg.apps.orderform.excel_orderform_parser import ExcelOrderformParser from cg.constants import Pipeline @@ -8,9 +7,7 @@ from cg.models.orders.orderform_schema import Orderform -def get_sample_obj( - order_form_parser: ExcelOrderformParser, sample_id: str -) -> Optional[ExcelSample]: +def get_sample_obj(order_form_parser: ExcelOrderformParser, sample_id: str) -> ExcelSample | None: for sample_obj in order_form_parser.samples: if sample_obj.name == sample_id: return sample_obj diff --git a/tests/cli/upload/conftest.py b/tests/cli/upload/conftest.py index c7a97ba53a..3bac71741b 100644 --- a/tests/cli/upload/conftest.py +++ b/tests/cli/upload/conftest.py @@ -4,7 +4,6 @@ from datetime import datetime from pathlib import Path from tempfile import tempdir -from typing import Union import pytest @@ -293,7 +292,7 @@ def lims_samples() -> dict: ) return lims_case["samples"] - def sample(self, sample_id) -> Union[str, None]: + def sample(self, sample_id) -> str | None: """Returns a lims sample matching the provided sample_id""" for sample in self.lims_samples(): if sample["id"] == sample_id: diff --git a/tests/conftest.py b/tests/conftest.py index 8c3746c1b5..a1c0c01a7b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,7 @@ from datetime import MAXYEAR, datetime, timedelta from pathlib import Path from subprocess import CompletedProcess -from typing import Any, Generator, Union +from typing import Any, Generator import pytest from housekeeper.store.models import File, Version @@ -3160,7 +3160,7 @@ def store_with_sequencing_metrics( helpers: StoreHelpers, ) -> Store: """Return a store with multiple samples with sample lane sequencing metrics.""" - sample_sequencing_metrics_details: list[Union[str, str, int, int, float, int]] = [ + sample_sequencing_metrics_details: list[str | int | float] = [ (sample_id, flow_cell_name, 1, expected_total_reads / 2, 90.5, 32), (sample_id, flow_cell_name, 2, expected_total_reads / 2, 90.4, 31), (mother_sample_id, flow_cell_name_demultiplexed_with_bcl2fastq, 2, 2_000_000, 85.5, 30), diff --git a/tests/meta/clean/conftest.py b/tests/meta/clean/conftest.py index bd423ca5df..c46258d053 100644 --- a/tests/meta/clean/conftest.py +++ b/tests/meta/clean/conftest.py @@ -1,7 +1,6 @@ """Tests for the CleanFlowCellsAPI.""" from datetime import datetime from pathlib import Path -from typing import Union import pytest @@ -85,7 +84,7 @@ def store_with_flow_cell_to_clean( helpers: StoreHelpers, ) -> Store: """Return a store with multiple samples with sample lane sequencing metrics.""" - sample_sequencing_metrics_details: list[Union[str, str, int, int, float, int]] = [ + sample_sequencing_metrics_details: list[str | int | float] = [ (sample_id, tmp_flow_cell_to_clean.id, 1, 50_000_0000, 90.5, 32), (sample_id, tmp_flow_cell_to_clean.id, 2, 50_000_0000, 90.4, 31), ] @@ -114,7 +113,7 @@ def store_with_flow_cell_not_to_clean( helpers: StoreHelpers, ) -> Store: """Return a store with multiple samples with sample lane sequencing metrics.""" - sample_sequencing_metrics_details: list[Union[str, str, int, int, float, int]] = [ + sample_sequencing_metrics_details: list[str | int | float] = [ (sample_id, tmp_flow_cell_not_to_clean.id, 1, 50_000_0000, 90.5, 32), (sample_id, tmp_flow_cell_not_to_clean.id, 2, 50_000_0000, 90.4, 31), ] diff --git a/tests/meta/demultiplex/test_delete_demultiplex_api.py b/tests/meta/demultiplex/test_delete_demultiplex_api.py index cc551f59c1..32da80caba 100644 --- a/tests/meta/demultiplex/test_delete_demultiplex_api.py +++ b/tests/meta/demultiplex/test_delete_demultiplex_api.py @@ -1,6 +1,5 @@ import logging from pathlib import Path -from typing import Optional import pytest @@ -114,9 +113,9 @@ def test_no_active_samples_on_flow_cell( # WHEN checking for active samples on flowcell populated_delete_demultiplex_api._set_samples_on_flow_cell() - active_samples_on_flow_cell: Optional[ - list[str] - ] = populated_delete_demultiplex_api.active_samples_on_flow_cell() + active_samples_on_flow_cell: list[ + str + ] | None = populated_delete_demultiplex_api.active_samples_on_flow_cell() # THEN the no samples on the flowcell should be found active assert not active_samples_on_flow_cell @@ -140,9 +139,9 @@ def test_active_samples_on_flow_cell( # WHEN checking for active samples on flowcell active_delete_demultiplex_api._set_samples_on_flow_cell() - active_samples_on_flow_cell: Optional[ - list[str] - ] = active_delete_demultiplex_api.active_samples_on_flow_cell() + active_samples_on_flow_cell: list[ + str + ] | None = active_delete_demultiplex_api.active_samples_on_flow_cell() # THEN there should be active samples found assert any(sample.internal_id in active_samples_on_flow_cell for sample in samples_on_flow_cell) diff --git a/tests/meta/observations/conftest.py b/tests/meta/observations/conftest.py index 5704942294..91a57df8ec 100644 --- a/tests/meta/observations/conftest.py +++ b/tests/meta/observations/conftest.py @@ -1,6 +1,5 @@ """Fixtures for observations.""" -from typing import Optional import pytest @@ -42,14 +41,14 @@ def load(*args, **kwargs) -> dict: return dict(variants=15) @staticmethod - def get_case(*args, **kwargs) -> Optional[dict]: + def get_case(*args, **kwargs) -> dict | None: """Mock get_case method.""" _ = args _ = kwargs return {"case_id": "case_id", LOQUSDB_ID: "123"} @staticmethod - def get_duplicate(*args, **kwargs) -> Optional[dict]: + def get_duplicate(*args, **kwargs) -> dict | None: """Mock get_duplicate method.""" _ = args _ = kwargs diff --git a/tests/mocks/hk_mock.py b/tests/mocks/hk_mock.py index 7d068be027..2a6aded99b 100644 --- a/tests/mocks/hk_mock.py +++ b/tests/mocks/hk_mock.py @@ -5,7 +5,7 @@ import tempfile from contextlib import contextmanager from pathlib import Path -from typing import Optional, Set +from typing import Set from housekeeper.store.models import Bundle, File, Version @@ -183,7 +183,7 @@ def get_latest_file_from_version(self, version: Version, tags: Set[str]): return None return self._files[-1] - def get_file_from_latest_version(self, bundle_name: str, tags: list[str]) -> Optional[File]: + def get_file_from_latest_version(self, bundle_name: str, tags: list[str]) -> File | None: """Find a file in the latest version of a bundle.""" version: Version = self.last_version(bundle=bundle_name) if not version: @@ -191,9 +191,7 @@ def get_file_from_latest_version(self, bundle_name: str, tags: list[str]) -> Opt raise HousekeeperBundleVersionMissingError return self.files(version=version.id, tags=tags).first() - def get_files_from_latest_version( - self, bundle_name: str, tags: list[str] - ) -> Optional[list[File]]: + def get_files_from_latest_version(self, bundle_name: str, tags: list[str]) -> list[File] | None: """Return files in the latest version of a bundle.""" version: Version = self.last_version(bundle=bundle_name) if not version: @@ -465,7 +463,7 @@ def add_file(self, path, version_obj, tags, to_archive=False): return new_file def check_bundle_files( - self, bundle_name: str, file_paths: list[Path], last_version, tags: Optional[list] = None + self, bundle_name: str, file_paths: list[Path], last_version, tags: list | None = None ) -> list[Path]: """Checks if any of the files in the provided list are already added to the provided bundle. Returns a list of files that have not been added""" for file in self.get_files(bundle=bundle_name, tags=tags, version=last_version.id): @@ -512,7 +510,7 @@ def is_fastq_or_spring_in_all_bundles(self, bundle_names: list[str]) -> bool: sequencing_files_in_hk[bundle_name] = False for tag in [SequencingFileTag.FASTQ, SequencingFileTag.SPRING_METADATA]: sample_file_in_hk: list[bool] = [] - hk_files: Optional[list[File]] = self.get_files_from_latest_version( + hk_files: list[File] | None = self.get_files_from_latest_version( bundle_name=bundle_name, tags=[tag] ) sample_file_in_hk += [True for hk_file in hk_files if hk_file.is_included] @@ -523,7 +521,7 @@ def is_fastq_or_spring_in_all_bundles(self, bundle_names: list[str]) -> bool: ) return all(sequencing_files_in_hk.values()) - def delete_file(self, file_id: int) -> Optional[File]: + def delete_file(self, file_id: int) -> File | None: """Mock deleting a file both from database and disk (if included).""" file_obj = self._get_mock_file(file_id) if not file_obj: @@ -539,7 +537,7 @@ def delete_file(self, file_id: int) -> Optional[File]: return file_obj - def _get_mock_file(self, file_id: int) -> Optional[File]: + def _get_mock_file(self, file_id: int) -> File | None: for file_obj in self._files: if file_obj.id == file_id: return file_obj diff --git a/tests/mocks/limsmock.py b/tests/mocks/limsmock.py index 9c17a33fb7..1c2a6fc4e1 100644 --- a/tests/mocks/limsmock.py +++ b/tests/mocks/limsmock.py @@ -1,5 +1,3 @@ -from typing import Optional - from pydantic.v1 import BaseModel from typing_extensions import Literal @@ -64,7 +62,7 @@ def set_sequencing_method(self, method: str = "1338:00 Test sequencing method"): """Mock function""" self._prep_method = method - def sample(self, sample_id: str) -> Optional[dict]: + def sample(self, sample_id: str) -> dict | None: return next((sample for sample in self._samples if sample["id"] == sample_id), None) def add_sample(self, internal_id: str): diff --git a/tests/mocks/osticket.py b/tests/mocks/osticket.py index b33cfeef1b..83a7edb8fe 100644 --- a/tests/mocks/osticket.py +++ b/tests/mocks/osticket.py @@ -2,7 +2,6 @@ import logging import os.path -from typing import Optional from flask import Flask @@ -45,7 +44,7 @@ def setup( def open_ticket( self, attachment: dict, email: str, message: str, name: str, subject: str - ) -> Optional[str]: + ) -> str | None: """Open a new ticket through the REST API.""" if self._should_fail: LOG.error("res.text: %s, reason: %s", self._ticket_nr, "Unknown reason") diff --git a/tests/mocks/report.py b/tests/mocks/report.py index 9f851fd834..1c1a48b990 100644 --- a/tests/mocks/report.py +++ b/tests/mocks/report.py @@ -2,7 +2,6 @@ import logging from datetime import datetime from pathlib import Path -from typing import Optional from housekeeper.store.models import Version @@ -89,7 +88,7 @@ def __init__(self): mock_config = {"chanjo": {"config_path": "/mock/path", "binary_path": "/mock/path"}} super().__init__(mock_config) - def sample_coverage(self, sample_id: str, panel_genes: list) -> Optional[dict[str, float]]: + def sample_coverage(self, sample_id: str, panel_genes: list) -> dict[str, float] | None: """Return mocked sample dictionary.""" sample_coverage = None if sample_id == "ADM1": diff --git a/tests/mocks/tb_mock.py b/tests/mocks/tb_mock.py index d8543d6f56..1bb09f7019 100644 --- a/tests/mocks/tb_mock.py +++ b/tests/mocks/tb_mock.py @@ -1,5 +1,3 @@ -from typing import Optional - from cg.apps.tb.models import TrailblazerAnalysis @@ -26,7 +24,7 @@ def has_latest_analysis_started(self, *args, **kwargs) -> bool: def analyses(self, *args, **kwargs) -> list: return self.analyses_response - def get_latest_analysis(self, case_id: str) -> Optional[TrailblazerAnalysis]: + def get_latest_analysis(self, case_id: str) -> TrailblazerAnalysis | None: return self.get_latest_analysis_response.get(case_id) def ensure_analyses_response(self, analyses_list: list) -> None: diff --git a/tests/store/api/find/test_find_basic_data_application_version.py b/tests/store/api/find/test_find_basic_data_application_version.py index ce131fb610..9aa3c41eef 100644 --- a/tests/store/api/find/test_find_basic_data_application_version.py +++ b/tests/store/api/find/test_find_basic_data_application_version.py @@ -1,5 +1,3 @@ -from typing import Optional - from cg.store import Store from cg.store.models import Application, ApplicationVersion @@ -31,9 +29,9 @@ def test_get_current_application_version_by_tag_invalid_tag( assert invalid_application_tag not in tags # WHEN getting the current application version by tag - application_version: Optional[ - ApplicationVersion - ] = base_store.get_current_application_version_by_tag(tag=invalid_application_tag) + application_version: ApplicationVersion | None = ( + base_store.get_current_application_version_by_tag(tag=invalid_application_tag) + ) # THEN the application version is None assert application_version is None diff --git a/tests/store/api/find/test_find_business_data.py b/tests/store/api/find/test_find_business_data.py index 93e2c75f35..d9057bff21 100644 --- a/tests/store/api/find/test_find_business_data.py +++ b/tests/store/api/find/test_find_business_data.py @@ -1,7 +1,6 @@ """Tests the findbusinessdata part of the Cg store API.""" import logging from datetime import datetime -from typing import Optional import pytest from sqlalchemy.orm import Query @@ -896,7 +895,7 @@ def test_get_number_of_reads_for_sample_passing_q30_threshold( metrics: Query = store_with_sequencing_metrics._get_query(table=SampleLaneSequencingMetrics) # GIVEN a metric for a specific sample - sample_metric: Optional[SampleLaneSequencingMetrics] = metrics.filter( + sample_metric: SampleLaneSequencingMetrics | None = metrics.filter( SampleLaneSequencingMetrics.sample_internal_id == sample_id ).first() assert sample_metric diff --git a/tests/store/api/status/test_store_api_status_analysis.py b/tests/store/api/status/test_store_api_status_analysis.py index a2b8e74d14..33a87de38e 100644 --- a/tests/store/api/status/test_store_api_status_analysis.py +++ b/tests/store/api/status/test_store_api_status_analysis.py @@ -1,6 +1,5 @@ """This script tests the cli methods to add cases to status-db""" from datetime import datetime -from typing import Union from sqlalchemy.orm import Query @@ -224,7 +223,7 @@ def test_all_samples_and_analysis_completed( test_analysis: Analysis = helpers.add_analysis(base_store, completed_at=timestamp_now) # Given a completed analysis - test_analysis.case.action: Union[None, str] = None + test_analysis.case.action: str | None = None # GIVEN a database with a case with one of one sequenced samples and completed analysis link = base_store.relate_sample(test_analysis.case, test_sample, PhenotypeStatus.UNKNOWN) diff --git a/tests/store/filters/test_status_cases_filters.py b/tests/store/filters/test_status_cases_filters.py index 7221307329..b028ed8946 100644 --- a/tests/store/filters/test_status_cases_filters.py +++ b/tests/store/filters/test_status_cases_filters.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import Union from sqlalchemy.orm import Query @@ -367,7 +366,7 @@ def test_filter_cases_for_analysis_when_cases_with_no_action_and_new_sequence_da test_analysis: Analysis = helpers.add_analysis(base_store, pipeline=Pipeline.MIP_DNA) # Given an action set to None - test_analysis.case.action: Union[None, str] = None + test_analysis.case.action = None # GIVEN a database with a case with one sequenced samples for specified analysis link = base_store.relate_sample(test_analysis.case, test_sample, PhenotypeStatus.UNKNOWN) @@ -403,7 +402,7 @@ def test_filter_cases_for_analysis_when_cases_with_no_action_and_old_sequence_da test_analysis: Analysis = helpers.add_analysis(base_store, pipeline=Pipeline.MIP_DNA) # Given an action set to None - test_analysis.case.action: Union[None, str] = None + test_analysis.case.action: str | None = None # GIVEN a database with a case with one sequenced samples for specified analysis link = base_store.relate_sample(test_analysis.case, test_sample, PhenotypeStatus.UNKNOWN) diff --git a/tests/store/filters/test_status_flow_cell_filters.py b/tests/store/filters/test_status_flow_cell_filters.py index 3f775703db..f58320c03b 100644 --- a/tests/store/filters/test_status_flow_cell_filters.py +++ b/tests/store/filters/test_status_flow_cell_filters.py @@ -1,5 +1,3 @@ -from typing import Optional - from sqlalchemy.orm import Query from cg.constants import FlowCellStatus @@ -29,7 +27,7 @@ def test_get_flow_cells_by_case( # GIVEN a flow cell Query # WHEN getting flow cell - returned_flow_cell: Optional[list[Flowcell]] = filter_flow_cells_by_case( + returned_flow_cell: list[Flowcell] | None = filter_flow_cells_by_case( flow_cells=base_store._get_join_flow_cell_sample_links_query(), case=case ) @@ -49,7 +47,7 @@ def test_get_flow_cells_by_case_when_no_flow_cell_for_case( # GIVEN a flow cell Query # WHEN getting flow cell - returned_flow_cell: Optional[list[Flowcell]] = filter_flow_cells_by_case( + returned_flow_cell: list[Flowcell] | None = filter_flow_cells_by_case( flow_cells=base_store._get_join_flow_cell_sample_links_query(), case=case ) diff --git a/tests/store/filters/test_status_metrics_filters.py b/tests/store/filters/test_status_metrics_filters.py index 2072382f64..3652d4954b 100644 --- a/tests/store/filters/test_status_metrics_filters.py +++ b/tests/store/filters/test_status_metrics_filters.py @@ -1,5 +1,3 @@ -from typing import Optional - from sqlalchemy.orm import Query from cg.store import Store @@ -11,7 +9,9 @@ filter_total_read_count_for_sample, ) from cg.store.models import SampleLaneSequencingMetrics -from tests.meta.demultiplex.conftest import flow_cell_name_demultiplexed_with_bcl_convert +from tests.meta.demultiplex.conftest import ( + flow_cell_name_demultiplexed_with_bcl_convert, +) def test_filter_total_read_count_for_sample( @@ -29,7 +29,7 @@ def test_filter_total_read_count_for_sample( assert isinstance(total_reads_query, Query) # THEN a total reads count is returned - actual_total_reads: Optional[int] = total_reads_query.scalar() + actual_total_reads: int | None = total_reads_query.scalar() assert actual_total_reads # THEN assert that the actual total read count is as expected @@ -114,7 +114,7 @@ def test_filter_metrics_by_sample_internal_id(store_with_sequencing_metrics: Sto def test_filter_above_q30_threshold(store_with_sequencing_metrics: Store): # GIVEN a Store with sequencing metrics metrics: Query = store_with_sequencing_metrics._get_query(table=SampleLaneSequencingMetrics) - metric: Optional[SampleLaneSequencingMetrics] = metrics.first() + metric: SampleLaneSequencingMetrics | None = metrics.first() assert metric # GIVEN a Q30 threshold that at least one metric will pass diff --git a/tests/store_helpers.py b/tests/store_helpers.py index 438a0f06ea..0eb846cdfc 100644 --- a/tests/store_helpers.py +++ b/tests/store_helpers.py @@ -1,7 +1,6 @@ """Utility functions to simply add test data in a cg store.""" import logging from datetime import datetime -from typing import Optional from housekeeper.store.models import Bundle, Version @@ -19,10 +18,10 @@ ApplicationVersion, Bed, BedVersion, - Collaboration, - Customer, Case, CaseSample, + Collaboration, + Customer, Flowcell, Invoice, Organism, @@ -227,7 +226,7 @@ def ensure_application_limitation( @staticmethod def ensure_bed_version(store: Store, bed_name: str = "dummy_bed") -> BedVersion: """Return existing or create and return bed version for tests.""" - bed: Optional[Bed] = store.get_bed_by_name(bed_name) + bed: Bed | None = store.get_bed_by_name(bed_name) if not bed: bed: Bed = store.add_bed(name=bed_name) store.session.add(bed) @@ -426,7 +425,7 @@ def add_case( ) if not case_obj: - case_obj: Optional[Case] = store.get_case_by_internal_id(internal_id=name) + case_obj: Case | None = store.get_case_by_internal_id(internal_id=name) if not case_obj: case_obj = store.add_case( data_analysis=data_analysis, @@ -620,10 +619,10 @@ def add_flow_cell( samples: list[Sample] = None, status: str = None, date: datetime = datetime.now(), - has_backup: Optional[bool] = False, + has_backup: bool | None = False, ) -> Flowcell: """Utility function to add a flow cell to the store and return an object.""" - flow_cell: Optional[Flowcell] = store.get_flow_cell_by_name(flow_cell_name=flow_cell_name) + flow_cell: Flowcell | None = store.get_flow_cell_by_name(flow_cell_name=flow_cell_name) if flow_cell: return flow_cell flow_cell: Flowcell = store.add_flow_cell( @@ -663,7 +662,7 @@ def add_relationship( @staticmethod def add_synopsis_to_case( store: Store, case_id: str, synopsis: str = "a synopsis" - ) -> Optional[Case]: + ) -> Case | None: """Function for adding a synopsis to a case in the database.""" case_obj: Case = store.get_case_by_internal_id(internal_id=case_id) if not case_obj: @@ -676,7 +675,7 @@ def add_synopsis_to_case( @staticmethod def add_phenotype_groups_to_sample( store: Store, sample_id: str, phenotype_groups: [str] = None - ) -> Optional[Sample]: + ) -> Sample | None: """Function for adding a phenotype group to a sample in the database.""" if phenotype_groups is None: phenotype_groups = ["a phenotype group"] @@ -691,7 +690,7 @@ def add_phenotype_groups_to_sample( @staticmethod def add_phenotype_terms_to_sample( store: Store, sample_id: str, phenotype_terms: list[str] = [] - ) -> Optional[Sample]: + ) -> Sample | None: """Function for adding a phenotype term to a sample in the database.""" if not phenotype_terms: phenotype_terms: list[str] = ["a phenotype term"] @@ -706,7 +705,7 @@ def add_phenotype_terms_to_sample( @staticmethod def add_subject_id_to_sample( store: Store, sample_id: str, subject_id: str = "a subject_id" - ) -> Optional[Sample]: + ) -> Sample | None: """Function for adding a subject_id to a sample in the database.""" sample_obj: Sample = store.get_sample_by_internal_id(internal_id=sample_id) if not sample_obj: @@ -827,9 +826,9 @@ def ensure_invoice( invoice_id: int = 0, customer_id: str = "cust000", discount: int = 0, - pools: Optional[list[Pool]] = None, - samples: Optional[list[Sample]] = None, - invoiced_at: Optional[datetime] = None, + pools: list[Pool] | None = None, + samples: list[Sample] | None = None, + invoiced_at: datetime | None = None, ) -> Invoice: """Utility function to create an invoice with a costumer and samples or pools.""" invoice = store.get_invoice_by_entry_id(entry_id=invoice_id)