Skip to content

Commit

Permalink
Patch existing parents in rules
Browse files Browse the repository at this point in the history
  • Loading branch information
islean committed Jan 28, 2025
1 parent 3303c0e commit 04e3d3b
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 38 deletions.
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:
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]
4 changes: 2 additions & 2 deletions cg/services/orders/validation/workflows/tomte/models/case.py
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 father'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

0 comments on commit 04e3d3b

Please sign in to comment.