Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Nallo workflow metrics-deliver #4142

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cg/cli/workflow/nallo/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from cg.cli.utils import CLICK_CONTEXT_SETTINGS

from cg.cli.workflow.nf_analysis import config_case, run, start
from cg.cli.workflow.nf_analysis import config_case, run, start, metrics_deliver

from cg.constants.constants import MetaApis
from cg.meta.workflow.analysis import AnalysisAPI
Expand All @@ -26,3 +26,4 @@ def nallo(context: click.Context) -> None:
nallo.add_command(config_case)
nallo.add_command(run)
nallo.add_command(start)
nallo.add_command(metrics_deliver)
4 changes: 4 additions & 0 deletions cg/constants/nf_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class NfTowerStatus(StrEnum):
UNKNOWN: str = "UNKNOWN"


NALLO_METRIC_CONDITIONS: dict[str, dict[str, Any]] = {
"median_coverage": {"norm": "gt", "threshold": 25},
}

RAREDISEASE_PREDICTED_SEX_METRIC = "predicted_sex_sex_check"

RAREDISEASE_METRIC_CONDITIONS: dict[str, dict[str, Any]] = {
Expand Down
5 changes: 5 additions & 0 deletions cg/meta/workflow/nallo.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Module for Nallo Analysis API."""

import logging

from cg.constants import Workflow
from cg.constants.nf_analysis import NALLO_METRIC_CONDITIONS
from cg.constants.subject import PlinkPhenotypeStatus, PlinkSex
from cg.meta.workflow.nf_analysis import NfAnalysisAPI
from cg.models.cg_config import CGConfig
Expand Down Expand Up @@ -91,3 +93,6 @@ def get_built_workflow_parameters(self, case_id: str) -> NalloParameters:
input=self.get_sample_sheet_path(case_id=case_id),
outdir=outdir,
)

def get_workflow_metrics(self, metric_id: str) -> dict:
return NALLO_METRIC_CONDITIONS
6 changes: 6 additions & 0 deletions cg/models/nallo/nallo.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
from cg.models.nf_analysis import WorkflowParameters


class NalloQCMetrics(BaseModel):
"""Nallo QC metrics"""

median_coverage: float | None


class NalloSampleSheetEntry(BaseModel):
"""Nallo sample model is used when building the sample sheet."""

Expand Down
6 changes: 3 additions & 3 deletions tests/cli/workflow/nf_analysis/test_cli_config_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def test_config_case_without_options(

@pytest.mark.parametrize(
"workflow",
NEXTFLOW_WORKFLOWS,
NEXTFLOW_WORKFLOWS + [Workflow.NALLO],
)
def test_config_with_missing_case(
cli_runner: CliRunner,
Expand Down Expand Up @@ -74,7 +74,7 @@ def test_config_with_missing_case(

@pytest.mark.parametrize(
"workflow",
NEXTFLOW_WORKFLOWS,
NEXTFLOW_WORKFLOWS + [Workflow.NALLO],
)
def test_config_case_without_samples(
cli_runner: CliRunner,
Expand Down Expand Up @@ -185,7 +185,7 @@ def test_config_case_default_parameters(

@pytest.mark.parametrize(
"workflow",
NEXTFLOW_WORKFLOWS,
NEXTFLOW_WORKFLOWS + [Workflow.NALLO],
)
def test_config_case_dry_run(
cli_runner: CliRunner,
Expand Down
24 changes: 21 additions & 3 deletions tests/cli/workflow/nf_analysis/test_cli_metrics_deliver.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@

@pytest.mark.parametrize(
"workflow",
[Workflow.RAREDISEASE, Workflow.RNAFUSION, Workflow.TAXPROFILER, Workflow.TOMTE],
[
Workflow.RAREDISEASE,
Workflow.RNAFUSION,
Workflow.TAXPROFILER,
Workflow.TOMTE,
Workflow.NALLO,
],
)
def test_metrics_deliver_without_options(
cli_runner: CliRunner, workflow: Workflow, request: FixtureRequest
Expand All @@ -38,7 +44,13 @@ def test_metrics_deliver_without_options(

@pytest.mark.parametrize(
"workflow",
[Workflow.RAREDISEASE, Workflow.RNAFUSION, Workflow.TAXPROFILER, Workflow.TOMTE],
[
Workflow.RAREDISEASE,
Workflow.RNAFUSION,
Workflow.TAXPROFILER,
Workflow.TOMTE,
Workflow.NALLO,
],
)
def test_metrics_deliver_with_missing_case(
cli_runner: CliRunner,
Expand Down Expand Up @@ -69,7 +81,13 @@ def test_metrics_deliver_with_missing_case(

@pytest.mark.parametrize(
"workflow",
[Workflow.RAREDISEASE, Workflow.RNAFUSION, Workflow.TAXPROFILER, Workflow.TOMTE],
[
Workflow.RAREDISEASE,
Workflow.RNAFUSION,
Workflow.TAXPROFILER,
Workflow.TOMTE,
Workflow.NALLO,
],
)
def test_metrics_deliver_case(
cli_runner: CliRunner,
Expand Down
1 change: 1 addition & 0 deletions tests/cli/workflow/test_cli_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def test_no_options(cli_runner: CliRunner, base_context: CGConfig):
assert "microsalt" in result.output
assert "mip-dna" in result.output
assert "mip-rna" in result.output
assert "nallo" in result.output
assert "raredisease" in result.output
assert "rnafusion" in result.output
assert "taxprofiler" in result.output
168 changes: 153 additions & 15 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,12 @@ def mip_dna_analysis_dir(mip_analysis_dir: Path) -> Path:
return Path(mip_analysis_dir, "dna")


@pytest.fixture
def nallo_analysis_dir(analysis_dir: Path) -> Path:
"""Return the path to the directory with nallo analysis files."""
return Path(analysis_dir, "nallo")


@pytest.fixture
def nf_analysis_analysis_dir(fixtures_dir: Path) -> Path:
"""Return the path to the directory with nf-analysis files."""
Expand Down Expand Up @@ -2115,7 +2121,7 @@ def context_config(
"workflow_bin_path": Path("workflow", "path").as_posix(),
"profile": "myprofile",
"references": Path("path", "to", "references").as_posix(),
"revision": "dev",
"revision": "2.2.0",
"root": str(nallo_dir),
"slurm": {
"account": "development",
Expand Down Expand Up @@ -2523,7 +2529,7 @@ def bam_unmapped_read_paths(housekeeper_dir: Path) -> Path:
"""Path to existing bam read file."""
bam_unmapped_read_path = Path(
housekeeper_dir, "m00000_000000_000000_s4.hifi_reads.bc2021"
).with_suffix(f"{AlignmentFileTag.BAM}")
).with_suffix(f".{AlignmentFileTag.BAM}")
with open(bam_unmapped_read_path, "wb") as wh:
wh.write(
b"1f 8b 08 04 00 00 00 00 00 ff 06 00 42 43 02 00 1b 00 03 00 00 00 00 00 00 00 00 00"
Expand All @@ -2541,7 +2547,7 @@ def sequencing_platform() -> str:
@pytest.fixture(scope="session")
def nallo_case_id() -> str:
"""Returns a nallo case id."""
return "nallo_case_two_samples"
return "nallo_case_enough_reads"


@pytest.fixture(scope="function")
Expand All @@ -2550,62 +2556,88 @@ def nallo_context(
helpers: StoreHelpers,
nf_analysis_housekeeper: HousekeeperAPI,
trailblazer_api: MockTB,
hermes_api: HermesApi,
cg_dir: Path,
nallo_case_id: str,
sample_id: str,
father_sample_id: str,
sample_name: str,
another_sample_name: str,
father_sample_id: str,
no_sample_case_id: str,
total_sequenced_reads_pass: int,
wgs_long_read_application_tag: str,
case_id_not_enough_reads: str,
sample_id_not_enough_reads: str,
total_sequenced_reads_not_pass: int,
) -> CGConfig:
"""Context to use in CLI."""
"""context to use in cli"""
cg_context.housekeeper_api_ = nf_analysis_housekeeper
cg_context.trailblazer_api_ = trailblazer_api
cg_context.meta_apis["analysis_api"] = NalloAnalysisAPI(config=cg_context)
status_db: Store = cg_context.status_db

# NB: the order in which the cases are added matters for the tests of store_available

# Create ERROR case with NO SAMPLES
helpers.add_case(status_db, internal_id=no_sample_case_id, name=no_sample_case_id)

# Create textbook case with two samples
nallo_case_two_samples: Case = helpers.add_case(
# Create textbook case with enough reads
case_enough_reads: Case = helpers.add_case(
store=status_db,
internal_id=nallo_case_id,
name=nallo_case_id,
data_analysis=Workflow.NALLO,
)

nallo_sample_one: Sample = helpers.add_sample(
sample_enough_reads: Sample = helpers.add_sample(
status_db,
internal_id=sample_id,
name=sample_name,
last_sequenced_at=datetime.now(),
reads=total_sequenced_reads_pass,
application_tag=wgs_long_read_application_tag,
reference_genome=GenomeVersion.HG38,
)

another_nallo_sample: Sample = helpers.add_sample(
another_sample_enough_reads: Sample = helpers.add_sample(
status_db,
internal_id=father_sample_id,
name=another_sample_name,
last_sequenced_at=datetime.now(),
reads=total_sequenced_reads_pass,
application_tag=wgs_long_read_application_tag,
reference_genome=GenomeVersion.HG38,
)

helpers.add_relationship(
status_db,
case=nallo_case_two_samples,
sample=nallo_sample_one,
case=case_enough_reads,
sample=sample_enough_reads,
)

helpers.add_relationship(
status_db,
case=nallo_case_two_samples,
sample=another_nallo_sample,
case=case_enough_reads,
sample=another_sample_enough_reads,
)

# Create case without enough reads
case_not_enough_reads: Case = helpers.add_case(
store=status_db,
internal_id=case_id_not_enough_reads,
name=case_id_not_enough_reads,
data_analysis=Workflow.NALLO,
)

sample_not_enough_reads: Sample = helpers.add_sample(
status_db,
internal_id=sample_id_not_enough_reads,
last_sequenced_at=datetime.now(),
reads=total_sequenced_reads_not_pass,
application_tag=wgs_long_read_application_tag,
reference_genome=GenomeVersion.HG38,
)

helpers.add_relationship(status_db, case=case_not_enough_reads, sample=sample_not_enough_reads)

return cg_context


Expand All @@ -2625,6 +2657,112 @@ def nallo_config(nallo_dir: Path, nallo_case_id: str) -> None:
).touch(exist_ok=True)


@pytest.fixture(scope="function")
def nallo_deliverable_data(nallo_dir: Path, nallo_case_id: str, sample_id: str) -> dict:
return {
"files": [
{
"path": f"{nallo_dir}/{nallo_case_id}/multiqc/multiqc_report.html",
"path_index": "",
"step": "report",
"tag": ["multiqc-html"],
"id": nallo_case_id,
"format": "html",
"mandatory": True,
},
]
}


@pytest.fixture(scope="function")
def nallo_metrics_deliverables(nallo_analysis_dir: Path) -> list[dict]:
"""Returns the content of a mock metrics deliverables file."""
return read_yaml(
file_path=Path(nallo_analysis_dir, "nallo_fixture_for_metrics_deliverables.yaml")
)


@pytest.fixture(scope="function")
def nallo_metrics_deliverables_path(nallo_dir: Path, nallo_case_id: str) -> Path:
"""Path to deliverables file."""
return Path(nallo_dir, nallo_case_id, f"{nallo_case_id}_metrics_deliverables").with_suffix(
FileExtensions.YAML
)


@pytest.fixture(scope="function")
def nallo_mock_analysis_finish(
nallo_dir: Path,
nallo_case_id: str,
nallo_multiqc_json_metrics: dict,
tower_id: int,
) -> None:
"""Create analysis finish file for testing."""
Path.mkdir(Path(nallo_dir, nallo_case_id, "pipeline_info"), parents=True, exist_ok=True)
Path(nallo_dir, nallo_case_id, "pipeline_info", software_version_file).touch(exist_ok=True)
Path(nallo_dir, nallo_case_id, f"{nallo_case_id}_samplesheet.csv").touch(exist_ok=True)
Path.mkdir(
Path(nallo_dir, nallo_case_id, "multiqc", "multiqc_data"),
parents=True,
exist_ok=True,
)
write_json(
content=nallo_multiqc_json_metrics,
file_path=Path(
nallo_dir,
nallo_case_id,
"multiqc",
"multiqc_data",
"multiqc_data",
).with_suffix(FileExtensions.JSON),
)
write_yaml(
content={nallo_case_id: [tower_id]},
file_path=Path(
nallo_dir,
nallo_case_id,
"tower_ids",
).with_suffix(FileExtensions.YAML),
)


@pytest.fixture(scope="function")
def nallo_mock_deliverable_dir(
nallo_dir: Path, nallo_deliverable_data: dict, nallo_case_id: str
) -> Path:
"""Create nallo deliverable file with dummy data and files to deliver."""
Path.mkdir(
Path(nallo_dir, nallo_case_id),
parents=True,
exist_ok=True,
)
Path.mkdir(
Path(nallo_dir, nallo_case_id, "multiqc"),
parents=True,
exist_ok=True,
)
for report_entry in nallo_deliverable_data["files"]:
Path(report_entry["path"]).touch(exist_ok=True)
WriteFile.write_file_from_content(
content=nallo_deliverable_data,
file_format=FileFormat.JSON,
file_path=Path(nallo_dir, nallo_case_id, nallo_case_id + deliverables_yaml),
)
return nallo_dir


@pytest.fixture
def nallo_multiqc_json_metrics_path(nallo_analysis_dir: Path) -> Path:
"""Return Multiqc JSON file path for nallo."""
return Path(nallo_analysis_dir, multiqc_json_file)


@pytest.fixture(scope="function")
def nallo_multiqc_json_metrics(nallo_analysis_dir) -> dict:
"""Returns the content of a mock Multiqc JSON file."""
return read_json(file_path=Path(nallo_analysis_dir, multiqc_json_file))


@pytest.fixture(scope="function")
def nallo_nexflow_config_file_path(nallo_dir, nallo_case_id) -> Path:
"""Path to config file."""
Expand Down
Loading
Loading