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

Patch existing parents in rules #4160

Merged
merged 3 commits into from
Jan 28, 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
10 changes: 9 additions & 1 deletion cg/services/orders/validation/models/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from cg.services.orders.validation.models.discriminators import has_internal_id
from cg.services.orders.validation.models.existing_sample import ExistingSample
from cg.services.orders.validation.models.sample_aliases import SampleInCase
from cg.store.models import Sample as DbSample
from cg.store.store import Store

NewSample = Annotated[SampleInCase, Tag("new")]
ExistingSampleType = Annotated[ExistingSample, Tag("existing")]
Expand Down Expand Up @@ -45,11 +47,17 @@ def enumerated_existing_samples(self) -> list[tuple[int, ExistingSample]]:
samples.append((sample_index, sample))
return samples

def get_sample(self, sample_name: str) -> SampleInCase | None:
def get_new_sample(self, sample_name: str) -> SampleInCase | None:
for _, sample in self.enumerated_new_samples:
if sample.name == sample_name:
return sample

def get_existing_sample_from_db(self, sample_name: str, store: Store) -> DbSample | None:
for _, sample in self.enumerated_existing_samples:
islean marked this conversation as resolved.
Show resolved Hide resolved
db_sample: DbSample | None = store.get_sample_by_internal_id(sample.internal_id)
if db_sample and db_sample.name == sample_name:
return db_sample

@model_validator(mode="before")
def convert_empty_strings_to_none(cls, data):
if isinstance(data, dict):
Expand Down
9 changes: 9 additions & 0 deletions cg/services/orders/validation/rules/case/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from cg.services.orders.validation.models.case import Case
from cg.services.orders.validation.models.existing_case import ExistingCase
from cg.services.orders.validation.workflows.balsamic.models.case import BalsamicCase
from cg.services.orders.validation.workflows.balsamic_umi.models.case import BalsamicUmiCase
Expand Down Expand Up @@ -34,3 +35,11 @@ def is_case_not_from_collaboration(case: ExistingCase, customer_id: str, store:
db_case: DbCase | None = store.get_case_by_internal_id(case.internal_id)
customer: Customer | None = store.get_customer_by_internal_id(customer_id)
return db_case and customer and db_case.customer not in customer.collaborators


def is_sample_in_case(case: Case, sample_name: str, store: Store) -> bool:
if case.get_new_sample(sample_name):
return True
elif case.get_existing_sample_from_db(sample_name=sample_name, store=store):
return True
return False
23 changes: 12 additions & 11 deletions cg/services/orders/validation/rules/case_sample/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,48 +292,49 @@ def validate_sample_names_different_from_case_names(
return errors


def validate_fathers_are_male(order: OrderWithCases, **kwargs) -> list[InvalidFatherSexError]:
def validate_fathers_are_male(
order: OrderWithCases, store: Store, **kwargs
) -> list[InvalidFatherSexError]:
errors: list[InvalidFatherSexError] = []
for index, case in order.enumerated_new_cases:
case_errors: list[InvalidFatherSexError] = get_father_sex_errors(
case=case, case_index=index
case=case, case_index=index, store=store
)
errors.extend(case_errors)
return errors


def validate_fathers_in_same_case_as_children(
order: OrderWithCases, **kwargs
order: OrderWithCases, store: Store, **kwargs
) -> list[FatherNotInCaseError]:
errors: list[FatherNotInCaseError] = []
for index, case in order.enumerated_new_cases:
case_errors: list[FatherNotInCaseError] = get_father_case_errors(
case=case,
case_index=index,
case=case, case_index=index, store=store
)
errors.extend(case_errors)
return errors


def validate_mothers_are_female(order: OrderWithCases, **kwargs) -> list[InvalidMotherSexError]:
def validate_mothers_are_female(
order: OrderWithCases, store: Store, **kwargs
) -> list[InvalidMotherSexError]:
errors: list[InvalidMotherSexError] = []
for index, case in order.enumerated_new_cases:
case_errors: list[InvalidMotherSexError] = get_mother_sex_errors(
case=case,
case_index=index,
case=case, case_index=index, store=store
)
errors.extend(case_errors)
return errors


def validate_mothers_in_same_case_as_children(
order: OrderWithCases, **kwargs
order: OrderWithCases, store: Store, **kwargs
) -> list[MotherNotInCaseError]:
errors: list[MotherNotInCaseError] = []
for index, case in order.enumerated_new_cases:
case_errors: list[MotherNotInCaseError] = get_mother_case_errors(
case=case,
case_index=index,
case=case, case_index=index, store=store
)
errors.extend(case_errors)
return errors
Expand Down
46 changes: 28 additions & 18 deletions cg/services/orders/validation/rules/case_sample/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
SampleInCase,
SampleWithRelatives,
)
from cg.services.orders.validation.rules.case.utils import is_sample_in_case
from cg.services.orders.validation.rules.utils import (
get_concentration_interval,
has_sample_invalid_concentration,
Expand Down Expand Up @@ -112,21 +113,27 @@ def get_repeated_case_name_errors(order: OrderWithCases) -> list[RepeatedCaseNam


def get_father_sex_errors(
case: CaseContainingRelatives, case_index: int
case: CaseContainingRelatives, case_index: int, store: Store
) -> list[InvalidFatherSexError]:
errors: list[InvalidFatherSexError] = []
children: list[tuple[SampleWithRelatives, int]] = case.get_samples_with_father()
for child, child_index in children:
if is_father_sex_invalid(child=child, case=case):
if is_father_sex_invalid(child=child, case=case, store=store):
error: InvalidFatherSexError = create_father_sex_error(
case_index=case_index, sample_index=child_index
)
errors.append(error)
return errors


def is_father_sex_invalid(child: SampleWithRelatives, case: CaseContainingRelatives) -> bool:
father: SampleWithRelatives | None = case.get_sample(child.father)
def is_father_sex_invalid(
child: SampleWithRelatives, case: CaseContainingRelatives, store: Store
) -> bool:
father: SampleWithRelatives | None = case.get_new_sample(child.father)
if not father:
father: DbSample | None = case.get_existing_sample_from_db(
sample_name=child.father, store=store
)
return father and father.sex != Sex.MALE


Expand All @@ -135,14 +142,14 @@ def create_father_sex_error(case_index: int, sample_index: int) -> InvalidFather


def get_father_case_errors(
case: CaseContainingRelatives,
case_index: int,
case: CaseContainingRelatives, case_index: int, store: Store
) -> list[FatherNotInCaseError]:
errors: list[FatherNotInCaseError] = []
children: list[tuple[SampleWithRelatives, int]] = case.get_samples_with_father()
children: list[tuple[SampleWithRelatives | ExistingSample, int]] = (
case.get_samples_with_father()
)
for child, child_index in children:
father: SampleWithRelatives | None = case.get_sample(child.father)
if not father:
if not is_sample_in_case(case=case, sample_name=child.father, store=store):
error: FatherNotInCaseError = create_father_case_error(
case_index=case_index,
sample_index=child_index,
Expand All @@ -152,13 +159,12 @@ def get_father_case_errors(


def get_mother_sex_errors(
case: CaseContainingRelatives,
case_index: int,
case: CaseContainingRelatives, case_index: int, store: Store
) -> list[InvalidMotherSexError]:
errors: list[InvalidMotherSexError] = []
children: list[tuple[SampleWithRelatives, int]] = case.get_samples_with_mother()
for child, child_index in children:
if is_mother_sex_invalid(child=child, case=case):
if is_mother_sex_invalid(child=child, case=case, store=store):
error: InvalidMotherSexError = create_mother_sex_error(
case_index=case_index,
sample_index=child_index,
Expand All @@ -168,14 +174,12 @@ def get_mother_sex_errors(


def get_mother_case_errors(
case: CaseContainingRelatives,
case_index: int,
case: CaseContainingRelatives, case_index: int, store: Store
) -> list[MotherNotInCaseError]:
errors: list[MotherNotInCaseError] = []
children: list[tuple[SampleWithRelatives, int]] = case.get_samples_with_mother()
for child, child_index in children:
mother: SampleWithRelatives | None = case.get_sample(child.mother)
if not mother:
if not is_sample_in_case(case=case, sample_name=child.mother, store=store):
error: MotherNotInCaseError = create_mother_case_error(
case_index=case_index, sample_index=child_index
)
Expand All @@ -191,8 +195,14 @@ def create_mother_case_error(case_index: int, sample_index: int) -> MotherNotInC
return MotherNotInCaseError(case_index=case_index, sample_index=sample_index)


def is_mother_sex_invalid(child: SampleWithRelatives, case: CaseContainingRelatives) -> bool:
mother: SampleWithRelatives | None = case.get_sample(child.mother)
def is_mother_sex_invalid(
child: SampleWithRelatives, case: CaseContainingRelatives, store: Store
) -> bool:
mother: SampleWithRelatives | None = case.get_new_sample(child.mother)
if not mother:
mother: DbSample | None = case.get_existing_sample_from_db(
sample_name=child.mother, store=store
)
return mother and mother.sex != Sex.FEMALE


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ class MipDnaCase(Case):
synopsis: str | None = None
samples: list[Annotated[NewSample | OldSample, Discriminator(has_internal_id)]]

def get_samples_with_father(self) -> list[tuple[MipDnaSample, int]]:
def get_samples_with_father(self) -> list[tuple[MipDnaSample | ExistingSample, int]]:
return [(sample, index) for index, sample in self.enumerated_samples if sample.father]

def get_samples_with_mother(self) -> list[tuple[MipDnaSample, int]]:
def get_samples_with_mother(self) -> list[tuple[MipDnaSample | ExistingSample, int]]:
return [(sample, index) for index, sample in self.enumerated_samples if sample.mother]
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ class TomteCase(Case):
synopsis: str | None = None
samples: list[Annotated[NewSample | OldSample, Discriminator(has_internal_id)]]

def get_samples_with_father(self) -> list[tuple[TomteSample, int]]:
def get_samples_with_father(self) -> list[tuple[TomteSample | ExistingSample, int]]:
return [(sample, index) for index, sample in self.enumerated_samples if sample.father]

def get_samples_with_mother(self) -> list[tuple[TomteSample, int]]:
def get_samples_with_mother(self) -> list[tuple[TomteSample | ExistingSample, int]]:
return [(sample, index) for index, sample in self.enumerated_samples if sample.mother]
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from cg.constants import Sex
from cg.models.orders.sample_base import StatusEnum
from cg.services.orders.validation.errors.case_errors import (
InvalidGenePanelsError,
Expand All @@ -7,6 +8,7 @@
DescendantAsFatherError,
FatherNotInCaseError,
InvalidFatherSexError,
InvalidMotherSexError,
PedigreeError,
SampleIsOwnFatherError,
)
Expand All @@ -16,9 +18,11 @@
validate_fathers_are_male,
validate_fathers_in_same_case_as_children,
validate_gene_panels_exist,
validate_mothers_are_female,
validate_pedigree,
)
from cg.services.orders.validation.workflows.tomte.models.order import TomteOrder
from cg.store.models import Sample
from cg.store.store import Store
from tests.store_helpers import StoreHelpers

Expand Down Expand Up @@ -55,11 +59,13 @@ def test_repeated_gene_panels(valid_order: TomteOrder, store_with_panels: Store)
assert isinstance(errors[0], RepeatedGenePanelsError)


def test_father_must_be_male(order_with_invalid_father_sex: TomteOrder):
def test_father_must_be_male(order_with_invalid_father_sex: TomteOrder, base_store: Store):
# GIVEN an order with an incorrectly specified father

# WHEN validating the order
errors: list[InvalidFatherSexError] = validate_fathers_are_male(order_with_invalid_father_sex)
errors: list[InvalidFatherSexError] = validate_fathers_are_male(
order=order_with_invalid_father_sex, store=base_store
)

# THEN errors are returned
assert errors
Expand All @@ -68,13 +74,73 @@ def test_father_must_be_male(order_with_invalid_father_sex: TomteOrder):
assert isinstance(errors[0], InvalidFatherSexError)


def test_father_in_wrong_case(order_with_father_in_wrong_case: TomteOrder):
def test_existing_father_must_be_male(
valid_order: TomteOrder, store_with_multiple_cases_and_samples: Store
):
"""Tests that an order with a father which is a female sample in StatusDB gives an error."""

# GIVEN a sample in StatusDB with female sex
father_db_sample: Sample = store_with_multiple_cases_and_samples.session.query(Sample).first()
father_db_sample.sex = Sex.FEMALE
store_with_multiple_cases_and_samples.commit_to_store()

# GIVEN that an order has a corresponding existing sample in one of its cases
father_sample = ExistingSample(internal_id=father_db_sample.internal_id)
valid_order.cases[0].samples.append(father_sample)

# GIVEN that another sample in the order specifies the sample as its father
father_name = father_db_sample.name
valid_order.cases[0].samples[0].father = father_name

# WHEN validating the order
errors: list[InvalidFatherSexError] = validate_fathers_are_male(
order=valid_order, store=store_with_multiple_cases_and_samples
)

# THEN errors are returned
assert errors

# THEN the errors are about the father's sex
assert isinstance(errors[0], InvalidFatherSexError)


def test_existing_mother_must_be_female(
valid_order: TomteOrder, store_with_multiple_cases_and_samples: Store
):
"""Tests that an order with a mother which is a male sample in StatusDB gives an error."""

# GIVEN a sample in StatusDB with male sex
mother_db_sample: Sample = store_with_multiple_cases_and_samples.session.query(Sample).first()
mother_db_sample.sex = Sex.MALE
store_with_multiple_cases_and_samples.commit_to_store()

# GIVEN that an order has a corresponding existing sample in one of its cases
mother_sample = ExistingSample(internal_id=mother_db_sample.internal_id)
valid_order.cases[0].samples.append(mother_sample)

# GIVEN that another sample in the order specifies the sample as its mother
mother_name = mother_db_sample.name
valid_order.cases[0].samples[0].mother = mother_name

# WHEN validating the order
errors: list[InvalidMotherSexError] = validate_mothers_are_female(
order=valid_order, store=store_with_multiple_cases_and_samples
)

# THEN errors are returned
assert errors

# THEN the errors are about the mother's sex
assert isinstance(errors[0], InvalidMotherSexError)


def test_father_in_wrong_case(order_with_father_in_wrong_case: TomteOrder, base_store: Store):

# GIVEN an order with the father sample in the wrong case

# WHEN validating the order
errors: list[FatherNotInCaseError] = validate_fathers_in_same_case_as_children(
order_with_father_in_wrong_case
order=order_with_father_in_wrong_case, store=base_store
)

# THEN an error is returned
Expand Down
Loading