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

(Improve order flow) Add balsamic sample errors #4102

Merged
merged 6 commits into from
Jan 16, 2025
Merged
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
17 changes: 17 additions & 0 deletions cg/services/order_validation_service/errors/case_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,20 @@ class CaseDoesNotExistError(CaseError):
class MultipleSamplesInCaseError(CaseError):
field: str = "sample_errors"
message: str = "Multiple samples in the same case not allowed"


class MoreThanTwoSamplesInCaseError(CaseError):
field: str = "sample_errors"
message: str = "More than two samples in the same case not allowed"


class NumberOfNormalSamplesError(CaseError):
field: str = "sample_errors"


class DoubleNormalError(NumberOfNormalSamplesError):
message: str = "Only one non-tumour sample is allowed per case"


class DoubleTumourError(NumberOfNormalSamplesError):
message: str = "Only one tumour sample is allowed per case"
43 changes: 42 additions & 1 deletion cg/services/order_validation_service/rules/case/rules.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
from cg.services.order_validation_service.errors.case_errors import (
CaseDoesNotExistError,
CaseNameNotAvailableError,
DoubleNormalError,
DoubleTumourError,
MoreThanTwoSamplesInCaseError,
MultipleSamplesInCaseError,
NumberOfNormalSamplesError,
RepeatedCaseNameError,
RepeatedGenePanelsError,
)
from cg.services.order_validation_service.models.case import Case
from cg.services.order_validation_service.models.order_with_cases import OrderWithCases
from cg.services.order_validation_service.rules.case.utils import contains_duplicates
from cg.services.order_validation_service.rules.case.utils import (
contains_duplicates,
is_double_normal,
is_double_tumour,
)
from cg.services.order_validation_service.rules.case_sample.utils import (
get_repeated_case_name_errors,
)
from cg.services.order_validation_service.workflows.balsamic.models.order import BalsamicOrder
from cg.services.order_validation_service.workflows.balsamic_umi.models.order import (
BalsamicUmiOrder,
)
from cg.store.store import Store


Expand Down Expand Up @@ -69,3 +81,32 @@ def validate_one_sample_per_case(
error = MultipleSamplesInCaseError(case_index=case_index)
errors.append(error)
return errors


def validate_at_most_two_samples_per_case(
order: OrderWithCases, **kwargs
) -> list[MoreThanTwoSamplesInCaseError]:
"""Validates that there is at most two samples in each case.
Only applicable to Balsamic and Balsamic-UMI."""
errors: list[MoreThanTwoSamplesInCaseError] = []
for case_index, case in order.enumerated_new_cases:
if len(case.samples) > 2:
error = MoreThanTwoSamplesInCaseError(case_index=case_index)
errors.append(error)
return errors


def validate_number_of_normal_samples(
order: BalsamicOrder | BalsamicUmiOrder, store: Store, **kwargs
) -> list[NumberOfNormalSamplesError]:
"""Validates that Balsamic cases with pairs of samples contain one tumour and one normal sample.
Only applicable to Balsamic and Balsamic-UMI."""
islean marked this conversation as resolved.
Show resolved Hide resolved
errors: list[NumberOfNormalSamplesError] = []
for case_index, case in order.enumerated_new_cases:
if is_double_normal(case=case, store=store):
error = DoubleNormalError(case_index=case_index)
errors.append(error)
elif is_double_tumour(case=case, store=store):
error = DoubleTumourError(case_index=case_index)
errors.append(error)
return errors
26 changes: 26 additions & 0 deletions cg/services/order_validation_service/rules/case/utils.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,28 @@
from cg.services.order_validation_service.workflows.balsamic.models.case import BalsamicCase
from cg.services.order_validation_service.workflows.balsamic_umi.models.case import BalsamicUmiCase
from cg.store.models import Sample
from cg.store.store import Store


def contains_duplicates(input_list: list) -> bool:
return len(set(input_list)) != len(input_list)


def is_double_tumour(case: BalsamicCase | BalsamicUmiCase, store: Store) -> bool:
return len(case.samples) == 2 and get_number_of_tumours(case=case, store=store) == 2


def is_double_normal(case: BalsamicCase | BalsamicUmiCase, store: Store) -> bool:
return len(case.samples) == 2 and get_number_of_tumours(case=case, store=store) == 0


def get_number_of_tumours(case: BalsamicCase | BalsamicUmiCase, store: Store) -> int:
number_of_tumours = 0
for sample in case.samples:
if sample.is_new and sample.tumour:
number_of_tumours += 1
elif not sample.is_new:
db_sample: Sample = store.get_sample_by_internal_id(sample.internal_id)
if db_sample.is_tumour:
number_of_tumours += 1
return number_of_tumours
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,8 @@
from cg.services.order_validation_service.models.discriminators import has_internal_id
from cg.services.order_validation_service.models.existing_case import ExistingCase
from cg.services.order_validation_service.models.order_with_cases import OrderWithCases
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.constants import BalsamicDeliveryType
from cg.services.order_validation_service.workflows.balsamic.models.case import BalsamicCase

NewCase = Annotated[BalsamicCase, Tag("new")]
OldCase = Annotated[ExistingCase, Tag("existing")]
Expand All @@ -18,3 +14,11 @@
class BalsamicOrder(OrderWithCases):
cases: list[Annotated[NewCase | OldCase, Discriminator(has_internal_id)]]
delivery_type: BalsamicDeliveryType

@property
def enumerated_new_cases(self) -> list[tuple[int, BalsamicCase | ExistingCase]]:
cases: list[tuple[int, BalsamicCase | ExistingCase]] = []
for case_index, case in self.enumerated_cases:
if case.is_new:
cases.append((case_index, case))
return cases
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from cg.services.order_validation_service.rules.case.rules import (
validate_at_most_two_samples_per_case,
islean marked this conversation as resolved.
Show resolved Hide resolved
validate_case_internal_ids_exist,
validate_case_names_available,
validate_case_names_not_repeated,
validate_number_of_normal_samples,
)
from cg.services.order_validation_service.rules.case_sample.rules import (
validate_application_compatibility,
Expand Down Expand Up @@ -30,9 +32,11 @@
)

BALSAMIC_CASE_RULES: list[callable] = [
validate_at_most_two_samples_per_case,
validate_case_internal_ids_exist,
validate_case_names_available,
validate_case_names_not_repeated,
validate_number_of_normal_samples,
]

BALSAMIC_CASE_SAMPLE_RULES: list[callable] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@
class BalsamicUmiOrder(BalsamicOrder):
cases: list[Annotated[NewCase | OldCase, Discriminator(has_internal_id)]]
delivery_type: BalsamicUmiDeliveryType

@property
def enumerated_new_cases(self) -> list[tuple[int, BalsamicUmiCase | ExistingCase]]:
cases: list[tuple[int, BalsamicUmiCase | ExistingCase]] = []
for case_index, case in self.enumerated_cases:
if case.is_new:
cases.append((case_index, case))
return cases
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,13 @@ def balsamic_validation_service(
@pytest.fixture
def balsamic_rule_set() -> RuleSet:
return ORDER_TYPE_RULE_SET_MAP[OrderType.BALSAMIC]


@pytest.fixture
def another_balsamic_sample() -> BalsamicSample:
return create_sample(2)


@pytest.fixture
def a_third_balsamic_sample() -> BalsamicSample:
return create_sample(3)
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
from cg.services.order_validation_service.errors.case_errors import (
DoubleNormalError,
DoubleTumourError,
MoreThanTwoSamplesInCaseError,
NumberOfNormalSamplesError,
)
from cg.services.order_validation_service.errors.case_sample_errors import CaptureKitMissingError
from cg.services.order_validation_service.rules.case.rules import (
validate_at_most_two_samples_per_case,
validate_number_of_normal_samples,
)
from cg.services.order_validation_service.workflows.balsamic.models.order import BalsamicOrder
from cg.services.order_validation_service.workflows.balsamic.models.sample import BalsamicSample
from cg.services.order_validation_service.workflows.balsamic.rules import (
validate_capture_kit_panel_requirement,
)
Expand All @@ -25,3 +36,70 @@ def test_validate_capture_kit_required(

# THEN the error should concern the missing capture kit
assert isinstance(errors[0], CaptureKitMissingError)


def test_more_than_two_samples_in_case(
valid_order: BalsamicOrder,
another_balsamic_sample: BalsamicSample,
a_third_balsamic_sample: BalsamicSample,
):
# GIVEN a Balsamic order with three samples in the same case

valid_order.cases[0].samples.append(another_balsamic_sample)
valid_order.cases[0].samples.append(a_third_balsamic_sample)

# WHEN validating that the order has at most one sample per case
errors: list[MoreThanTwoSamplesInCaseError] = validate_at_most_two_samples_per_case(valid_order)

# THEN an error should be returned
assert errors

# THEN the error should concern the multiple samples in the first case
assert isinstance(errors[0], MoreThanTwoSamplesInCaseError)
assert errors[0].case_index == 0


def test_double_tumour_samples_in_case(
valid_order: BalsamicOrder, another_balsamic_sample: BalsamicSample, base_store: Store
):
# GIVEN a Balsamic order with two samples in a case
valid_order.cases[0].samples.append(another_balsamic_sample)

# GIVEN that both samples are tumours
valid_order.cases[0].samples[0].tumour = True
valid_order.cases[0].samples[1].tumour = True

# WHEN validating that the order has at most one sample per case
errors: list[NumberOfNormalSamplesError] = validate_number_of_normal_samples(
order=valid_order, store=base_store
)

# THEN an error should be returned
assert errors

# THEN the error should concern the double tumours in the case
assert isinstance(errors[0], DoubleTumourError)
assert errors[0].case_index == 0


def test_double_normal_samples_in_case(
valid_order: BalsamicOrder, another_balsamic_sample: BalsamicSample, base_store: Store
):
# GIVEN a Balsamic order with two samples in a case
valid_order.cases[0].samples.append(another_balsamic_sample)

# GIVEN that both samples are tumours
valid_order.cases[0].samples[0].tumour = False
valid_order.cases[0].samples[1].tumour = False

# WHEN validating that the order has at most one sample per case
errors: list[NumberOfNormalSamplesError] = validate_number_of_normal_samples(
order=valid_order, store=base_store
)

# THEN an error should be returned
assert errors

# THEN the error should concern the double tumours in the case
assert isinstance(errors[0], DoubleNormalError)
assert errors[0].case_index == 0
Loading