Skip to content

Commit

Permalink
Add balsamic sample sex validation (#3665)
Browse files Browse the repository at this point in the history
  • Loading branch information
seallard authored Aug 30, 2024
1 parent 560e5c2 commit 0c9b4e4
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,8 @@ class InvalidVolumeError(CaseSampleError):
class InvalidBufferError(CaseSampleError):
field: str = "elution_buffer"
message: str = "The chosen buffer is not allowed when skipping reception control"


class SexSubjectIdError(CaseSampleError):
field: str = "sex"
message: str = "Another sample with the same subject id has a different sex"
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from cg.services.order_validation_service.errors.case_sample_errors import SexSubjectIdError
from cg.services.order_validation_service.workflows.balsamic.models.order import BalsamicOrder
from cg.services.order_validation_service.workflows.balsamic.rules.case_sample.utils import (
has_sex_and_subject,
)
from cg.store.store import Store


def validate_subject_sex_consistency(
order: BalsamicOrder,
store: Store,
) -> list[SexSubjectIdError]:
errors: list[SexSubjectIdError] = []

for case_index, case in order.enumerated_new_cases:
for sample_index, sample in case.enumerated_new_samples:
if not has_sex_and_subject(sample):
continue

if store.sample_exists_with_different_sex(
customer_internal_id=order.customer,
subject_id=sample.subject_id,
sex=sample.sex,
):
error = SexSubjectIdError(
case_index=case_index,
sample_index=sample_index,
)
errors.append(error)
return errors
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from cg.models.orders.sample_base import SexEnum
from cg.services.order_validation_service.workflows.balsamic.models.sample import (
BalsamicSample,
)


def has_sex_and_subject(sample: BalsamicSample) -> bool:
return bool(sample.subject_id and sample.sex != SexEnum.unknown)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
validate_volume_interval,
validate_wells_contain_at_most_one_sample,
)
from cg.services.order_validation_service.workflows.balsamic.rules.case_sample.rules import (
validate_subject_sex_consistency,
)


CASE_RULES: list[callable] = [
validate_case_internal_ids_exist,
Expand All @@ -43,4 +47,5 @@
validate_subject_ids_different_from_sample_names,
validate_volume_interval,
validate_wells_contain_at_most_one_sample,
validate_subject_sex_consistency,
]
36 changes: 28 additions & 8 deletions cg/store/crud/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
from cg.constants import SequencingRunDataAvailability, Workflow
from cg.constants.constants import CaseActions, CustomerId, PrepCategory, SampleType
from cg.exc import CaseNotFoundError, CgError, OrderNotFoundError, SampleNotFoundError
from cg.models.orders.sample_base import SexEnum
from cg.server.dto.orders.orders_request import OrdersRequest
from cg.server.dto.samples.collaborator_samples_request import CollaboratorSamplesRequest
from cg.server.dto.samples.collaborator_samples_request import (
CollaboratorSamplesRequest,
)
from cg.store.base import BaseHandler
from cg.store.exc import EntryNotFoundError
from cg.store.filters.status_analysis_filters import (
AnalysisFilter,
apply_analysis_filter,
Expand All @@ -21,9 +25,6 @@
ApplicationFilter,
apply_application_filter,
)
from cg.store.exc import EntryNotFoundError
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_application_limitations_filters import (
ApplicationLimitationsFilter,
apply_application_limitations_filter,
Expand Down Expand Up @@ -64,11 +65,13 @@
)
from cg.store.filters.status_invoice_filters import InvoiceFilter, apply_invoice_filter
from cg.store.filters.status_order_filters import OrderFilter, apply_order_filters

from cg.store.filters.status_organism_filters import OrganismFilter, apply_organism_filter
from cg.store.filters.status_organism_filters import (
OrganismFilter,
apply_organism_filter,
)
from cg.store.filters.status_pacbio_smrt_cell_filters import (
apply_pac_bio_smrt_cell_filters,
PacBioSMRTCellFilter,
apply_pac_bio_smrt_cell_filters,
)
from cg.store.filters.status_panel_filters import PanelFilter, apply_panel_filter
from cg.store.filters.status_pool_filters import PoolFilter, apply_pool_filter
Expand All @@ -91,12 +94,12 @@
Invoice,
Order,
Organism,
PacBioSMRTCell,
Panel,
Pool,
Sample,
SampleRunMetrics,
User,
PacBioSMRTCell,
)

LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -1562,3 +1565,20 @@ def get_case_ids_for_samples(self, sample_ids: list[int]) -> list[str]:
for sample_id in sample_ids:
case_ids.extend(self.get_case_ids_with_sample(sample_id))
return list(set(case_ids))

def sample_exists_with_different_sex(
self,
customer_internal_id: str,
subject_id: str,
sex: SexEnum,
) -> bool:
samples: list[Sample] = self.get_samples_by_customer_and_subject_id(
customer_internal_id=customer_internal_id,
subject_id=subject_id,
)
for sample in samples:
if sample.sex == SexEnum.unknown:
continue
if sample.sex != sex:
return True
return False
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import pytest

from cg.constants.constants import GenomeVersion, Workflow
from cg.models.orders.sample_base import ContainerEnum, ControlEnum, SexEnum, StatusEnum
from cg.services.order_validation_service.constants import MINIMUM_VOLUME
from cg.services.order_validation_service.workflows.balsamic.constants import BalsamicDeliveryType
from cg.services.order_validation_service.workflows.balsamic.models.case import BalsamicCase
from cg.services.order_validation_service.workflows.balsamic.models.order import BalsamicOrder
from cg.services.order_validation_service.workflows.balsamic.models.sample import BalsamicSample


def create_sample(id: int) -> BalsamicSample:
return BalsamicSample(
name=f"name{id}",
application="PANKTTR020",
container=ContainerEnum.plate,
container_name="ContainerName",
control=ControlEnum.not_control,
require_qc_ok=True,
reference_genome=GenomeVersion.HG19,
sex=SexEnum.female,
source="source",
status=StatusEnum.affected,
subject_id=f"subject{id}",
well_position=f"A:{id}",
volume=MINIMUM_VOLUME,
is_tumour=False,
)


def create_case(samples: list[BalsamicSample]) -> BalsamicCase:
return BalsamicCase(
name="name",
samples=samples,
)


def create_order(cases: list[BalsamicCase]) -> BalsamicOrder:
return BalsamicOrder(
connect_to_ticket=True,
delivery_type=BalsamicDeliveryType.FASTQ_ANALYSIS,
name="order_name",
ticket_number="#12345",
workflow=Workflow.BALSAMIC,
user_id=1,
customer="cust000",
cases=cases,
)


@pytest.fixture
def valid_order() -> BalsamicOrder:
sample = create_sample(1)
case = create_case([sample])
return create_order([case])
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from cg.models.orders.sample_base import SexEnum
from cg.services.order_validation_service.errors.case_sample_errors import SexSubjectIdError
from cg.services.order_validation_service.workflows.balsamic.models.order import BalsamicOrder
from cg.services.order_validation_service.workflows.balsamic.rules.case_sample.rules import (
validate_subject_sex_consistency,
)
from cg.store.models import Sample
from cg.store.store import Store


def test_validate_sex_subject_id_clash(valid_order: BalsamicOrder, sample_store: Store):
# GIVEN an existing sample
sample = sample_store.session.query(Sample).first()

# GIVEN an order and sample with the same customer and subject id
valid_order.customer = sample.customer.internal_id
valid_order.cases[0].samples[0].subject_id = "subject"
sample.subject_id = "subject"

# GIVEN a sample in the order that has a different sex
valid_order.cases[0].samples[0].sex = SexEnum.female
sample.sex = SexEnum.male

# WHEN validating the order
errors = validate_subject_sex_consistency(
order=valid_order,
store=sample_store,
)

# THEN an error should be given for the clash
assert errors
assert isinstance(errors[0], SexSubjectIdError)

0 comments on commit 0c9b4e4

Please sign in to comment.