Skip to content

Commit b62280e

Browse files
eliottBodiitaz93
andauthored
Add validations for Nallo orders (#4342)(minor)
## Description ### Added - Models and validation rules to make Nallo orderable in cg - Connect Nallo with CaseStoring service --------- Co-authored-by: Sebastian Diaz <[email protected]>
1 parent 6922e03 commit b62280e

File tree

22 files changed

+477
-28
lines changed

22 files changed

+477
-28
lines changed

cg/constants/constants.py

+16-16
Original file line numberDiff line numberDiff line change
@@ -165,22 +165,22 @@ class SampleType(StrEnum):
165165

166166

167167
class DataDelivery(StrEnum):
168-
ANALYSIS_FILES: str = "analysis"
169-
ANALYSIS_SCOUT: str = "analysis-scout"
170-
BAM: str = "bam"
171-
FASTQ: str = "fastq"
172-
FASTQ_SCOUT: str = "fastq-scout"
173-
FASTQ_QC: str = "fastq_qc"
174-
FASTQ_ANALYSIS: str = "fastq-analysis"
175-
FASTQ_QC_ANALYSIS: str = "fastq_qc-analysis"
176-
FASTQ_ANALYSIS_SCOUT: str = "fastq-analysis-scout"
177-
NIPT_VIEWER: str = "nipt-viewer"
178-
NO_DELIVERY: str = "no-delivery"
179-
RAW_DATA_ANALYSIS: str = "raw_data-analysis"
180-
RAW_DATA_ANALYSIS_SCOUT: str = "raw_data-analysis-scout"
181-
RAW_DATA_SCOUT: str = "raw_data-scout"
182-
SCOUT: str = "scout"
183-
STATINA: str = "statina"
168+
ANALYSIS_FILES = "analysis"
169+
ANALYSIS_SCOUT = "analysis-scout"
170+
BAM = "bam"
171+
FASTQ = "fastq"
172+
FASTQ_SCOUT = "fastq-scout"
173+
FASTQ_QC = "fastq_qc"
174+
FASTQ_ANALYSIS = "fastq-analysis"
175+
FASTQ_QC_ANALYSIS = "fastq_qc-analysis"
176+
FASTQ_ANALYSIS_SCOUT = "fastq-analysis-scout"
177+
NIPT_VIEWER = "nipt-viewer"
178+
NO_DELIVERY = "no-delivery"
179+
RAW_DATA_ANALYSIS = "raw_data-analysis"
180+
RAW_DATA_ANALYSIS_SCOUT = "raw_data-analysis-scout"
181+
RAW_DATA_SCOUT = "raw_data-scout"
182+
SCOUT = "scout"
183+
STATINA = "statina"
184184

185185

186186
class HastaSlurmPartitions(StrEnum):

cg/services/orders/constants.py

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
OrderType.MICROSALT: Workflow.MICROSALT,
1212
OrderType.MIP_DNA: Workflow.MIP_DNA,
1313
OrderType.MIP_RNA: Workflow.MIP_RNA,
14+
OrderType.NALLO: Workflow.NALLO,
1415
OrderType.PACBIO_LONG_READ: Workflow.RAW_DATA,
1516
OrderType.RML: Workflow.RAW_DATA,
1617
OrderType.RNAFUSION: Workflow.RNAFUSION,

cg/services/orders/storing/implementations/case_order_service.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ def store_order_data_in_status_db(self, order: OrderWithCases) -> list[DbCase]:
8585
)
8686

8787
db_order.cases.append(db_case)
88-
self.status_db.session.add_all(new_cases)
89-
self.status_db.session.add(db_order)
90-
self.status_db.session.commit()
88+
self.status_db.add_multiple_items_to_store(new_cases)
89+
self.status_db.add_item_to_store(db_order)
90+
self.status_db.commit_to_store()
9191
return new_cases
9292

9393
@staticmethod
@@ -129,7 +129,7 @@ def _create_db_sample(
129129
ordered: datetime,
130130
sample: SampleInCase,
131131
ticket: str,
132-
):
132+
) -> DbSample:
133133
application_tag = sample.application
134134
application_version: ApplicationVersion = (
135135
self.status_db.get_current_application_version_by_tag(tag=application_tag)
@@ -144,7 +144,7 @@ def _create_db_sample(
144144
**sample.model_dump(exclude={"application", "container", "container_name"}),
145145
)
146146
db_sample.customer = customer
147-
self.status_db.session.add(db_sample)
147+
self.status_db.add_item_to_store(db_sample)
148148
return db_sample
149149

150150
def _create_db_case(
@@ -214,7 +214,7 @@ def _create_db_sample_dict(
214214
case_samples: dict[str, DbSample] = {}
215215
for sample in case.samples:
216216
if sample.is_new:
217-
with self.status_db.session.no_autoflush:
217+
with self.status_db.no_autoflush_context():
218218
db_sample: DbSample = self._create_db_sample(
219219
case=case,
220220
customer=customer,

cg/services/orders/storing/service_registry.py

+4
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ def get_storing_service(self, order_type: OrderType) -> StoreOrderService:
7474
OrderLimsService,
7575
StoreCaseOrderService,
7676
),
77+
OrderType.NALLO: (
78+
OrderLimsService,
79+
StoreCaseOrderService,
80+
),
7781
OrderType.PACBIO_LONG_READ: (
7882
OrderLimsService,
7983
StorePacBioOrderService,

cg/services/orders/validation/errors/case_sample_errors.py

+5
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,8 @@ class BufferMissingError(CaseSampleError):
175175
class SampleOutsideOfCollaborationError(CaseSampleError):
176176
field: str = "internal_id"
177177
message: str = "Sample cannot be outside of collaboration"
178+
179+
180+
class SampleNameAlreadyExistsError(CaseSampleError):
181+
field: str = "name"
182+
message: str = "Sample name already exists in a previous order"

cg/services/orders/validation/order_type_maps.py

+10
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@
4242
)
4343
from cg.services.orders.validation.order_types.mutant.models.order import MutantOrder
4444
from cg.services.orders.validation.order_types.mutant.validation_rules import MUTANT_SAMPLE_RULES
45+
from cg.services.orders.validation.order_types.nallo.models.order import NalloOrder
46+
from cg.services.orders.validation.order_types.nallo.validation_rules import (
47+
NALLO_CASE_RULES,
48+
NALLO_CASE_SAMPLE_RULES,
49+
)
4550
from cg.services.orders.validation.order_types.order_validation_rules import ORDER_RULES
4651
from cg.services.orders.validation.order_types.pacbio_long_read.models.order import PacbioOrder
4752
from cg.services.orders.validation.order_types.pacbio_long_read.validation_rules import (
@@ -104,6 +109,10 @@ class RuleSet(BaseModel):
104109
case_rules=MIP_RNA_CASE_RULES,
105110
case_sample_rules=MIP_RNA_CASE_SAMPLE_RULES,
106111
),
112+
OrderType.NALLO: RuleSet(
113+
case_rules=NALLO_CASE_RULES,
114+
case_sample_rules=NALLO_CASE_SAMPLE_RULES,
115+
),
107116
OrderType.PACBIO_LONG_READ: RuleSet(
108117
sample_rules=PACBIO_LONG_READ_SAMPLE_RULES,
109118
),
@@ -136,6 +145,7 @@ class RuleSet(BaseModel):
136145
OrderType.MICROSALT: MicrosaltOrder,
137146
OrderType.MIP_DNA: MIPDNAOrder,
138147
OrderType.MIP_RNA: MIPRNAOrder,
148+
OrderType.NALLO: NalloOrder,
139149
OrderType.PACBIO_LONG_READ: PacbioOrder,
140150
OrderType.RML: RMLOrder,
141151
OrderType.RNAFUSION: RNAFusionOrder,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from enum import StrEnum
2+
3+
from cg.constants import DataDelivery
4+
5+
6+
class NalloDeliveryType(StrEnum):
7+
ANALYSIS = DataDelivery.ANALYSIS_FILES
8+
ANALYSIS_SCOUT = DataDelivery.ANALYSIS_SCOUT
9+
NO_DELIVERY = DataDelivery.NO_DELIVERY
10+
SCOUT = DataDelivery.SCOUT
11+
RAW_DATA_ANALYSIS = DataDelivery.RAW_DATA_ANALYSIS
12+
RAW_DATA_ANALYSIS_SCOUT = DataDelivery.RAW_DATA_ANALYSIS_SCOUT
13+
RAW_DATA_SCOUT = DataDelivery.RAW_DATA_SCOUT
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from pydantic import Discriminator, Tag
2+
from typing_extensions import Annotated
3+
4+
from cg.services.orders.validation.models.case import Case
5+
from cg.services.orders.validation.models.discriminators import has_internal_id
6+
from cg.services.orders.validation.models.existing_sample import ExistingSample
7+
from cg.services.orders.validation.order_types.nallo.models.sample import NalloSample
8+
9+
NewSample = Annotated[NalloSample, Tag("new")]
10+
OldSample = Annotated[ExistingSample, Tag("existing")]
11+
12+
13+
class NalloCase(Case):
14+
cohorts: list[str] | None = None
15+
panels: list[str]
16+
synopsis: str | None = None
17+
samples: list[Annotated[NewSample | OldSample, Discriminator(has_internal_id)]]
18+
19+
def get_samples_with_father(self) -> list[tuple[NalloSample | ExistingSample, int]]:
20+
return [(sample, index) for index, sample in self.enumerated_samples if sample.father]
21+
22+
def get_samples_with_mother(self) -> list[tuple[NalloSample | ExistingSample, int]]:
23+
return [(sample, index) for index, sample in self.enumerated_samples if sample.mother]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from pydantic import Discriminator, Tag
2+
from typing_extensions import Annotated
3+
4+
from cg.services.orders.validation.models.discriminators import has_internal_id
5+
from cg.services.orders.validation.models.existing_case import ExistingCase
6+
from cg.services.orders.validation.models.order_with_cases import OrderWithCases
7+
from cg.services.orders.validation.order_types.nallo.constants import NalloDeliveryType
8+
from cg.services.orders.validation.order_types.nallo.models.case import NalloCase
9+
from cg.services.orders.validation.order_types.nallo.models.sample import NalloSample
10+
11+
NewCase = Annotated[NalloCase, Tag("new")]
12+
OldCase = Annotated[ExistingCase, Tag("existing")]
13+
14+
15+
class NalloOrder(OrderWithCases):
16+
cases: list[Annotated[NewCase | OldCase, Discriminator(has_internal_id)]]
17+
delivery_type: NalloDeliveryType
18+
19+
@property
20+
def enumerated_new_samples(self) -> list[tuple[int, int, NalloSample]]:
21+
return [
22+
(case_index, sample_index, sample)
23+
for case_index, case in self.enumerated_new_cases
24+
for sample_index, sample in case.enumerated_new_samples
25+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import Annotated
2+
3+
from pydantic import BeforeValidator, Field
4+
5+
from cg.models.orders.sample_base import NAME_PATTERN, SexEnum, StatusEnum
6+
from cg.services.orders.validation.constants import ElutionBuffer
7+
from cg.services.orders.validation.models.sample import Sample
8+
from cg.services.orders.validation.utils import parse_buffer
9+
10+
11+
class NalloSample(Sample):
12+
elution_buffer: Annotated[ElutionBuffer | None, BeforeValidator(parse_buffer)] = None
13+
father: str | None = Field(None, pattern=NAME_PATTERN)
14+
mother: str | None = Field(None, pattern=NAME_PATTERN)
15+
phenotype_groups: list[str] | None = None
16+
phenotype_terms: list[str] | None = None
17+
require_qc_ok: bool = False
18+
sex: SexEnum
19+
source: str
20+
status: StatusEnum
21+
subject_id: str = Field(pattern=NAME_PATTERN, max_length=128)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from cg.services.orders.validation.rules.case.rules import (
2+
validate_case_internal_ids_exist,
3+
validate_case_names_available,
4+
validate_case_names_not_repeated,
5+
validate_each_new_case_has_an_affected_sample,
6+
validate_existing_cases_belong_to_collaboration,
7+
validate_existing_cases_have_an_affected_sample,
8+
validate_gene_panels_unique,
9+
)
10+
from cg.services.orders.validation.rules.case_sample.rules import (
11+
validate_application_compatibility,
12+
validate_application_exists,
13+
validate_application_not_archived,
14+
validate_container_name_required,
15+
validate_existing_samples_belong_to_collaboration,
16+
validate_fathers_are_male,
17+
validate_fathers_in_same_case_as_children,
18+
validate_gene_panels_exist,
19+
validate_mothers_are_female,
20+
validate_mothers_in_same_case_as_children,
21+
validate_not_all_samples_unknown_in_case,
22+
validate_pedigree,
23+
validate_sample_names_available,
24+
validate_sample_names_different_from_case_names,
25+
validate_sample_names_not_repeated,
26+
validate_samples_exist,
27+
validate_subject_ids_different_from_case_names,
28+
validate_subject_ids_different_from_sample_names,
29+
validate_subject_sex_consistency,
30+
validate_tube_container_name_unique,
31+
validate_volume_interval,
32+
validate_volume_required,
33+
validate_well_position_format,
34+
validate_well_positions_required,
35+
validate_wells_contain_at_most_one_sample,
36+
)
37+
38+
NALLO_CASE_RULES: list[callable] = [
39+
validate_case_internal_ids_exist,
40+
validate_case_names_available,
41+
validate_case_names_not_repeated,
42+
validate_each_new_case_has_an_affected_sample,
43+
validate_existing_cases_have_an_affected_sample,
44+
validate_existing_cases_belong_to_collaboration,
45+
validate_gene_panels_exist,
46+
validate_gene_panels_unique,
47+
]
48+
49+
NALLO_CASE_SAMPLE_RULES: list[callable] = [
50+
validate_application_compatibility,
51+
validate_application_exists,
52+
validate_application_not_archived,
53+
validate_container_name_required,
54+
validate_existing_samples_belong_to_collaboration,
55+
validate_fathers_are_male,
56+
validate_fathers_in_same_case_as_children,
57+
validate_mothers_are_female,
58+
validate_mothers_in_same_case_as_children,
59+
validate_not_all_samples_unknown_in_case,
60+
validate_pedigree,
61+
validate_samples_exist,
62+
validate_sample_names_available,
63+
validate_sample_names_different_from_case_names,
64+
validate_sample_names_not_repeated,
65+
validate_subject_ids_different_from_case_names,
66+
validate_subject_ids_different_from_sample_names,
67+
validate_subject_sex_consistency,
68+
validate_tube_container_name_unique,
69+
validate_volume_interval,
70+
validate_volume_required,
71+
validate_well_positions_required,
72+
validate_wells_contain_at_most_one_sample,
73+
validate_well_position_format,
74+
]

cg/services/orders/validation/rules/case/rules.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from cg.services.orders.validation.order_types.balsamic.models.order import BalsamicOrder
2020
from cg.services.orders.validation.order_types.balsamic_umi.models.order import BalsamicUmiOrder
2121
from cg.services.orders.validation.order_types.mip_dna.models.order import MIPDNAOrder
22+
from cg.services.orders.validation.order_types.nallo.models.order import NalloOrder
2223
from cg.services.orders.validation.rules.case.utils import (
2324
contains_duplicates,
2425
is_case_not_from_collaboration,
@@ -136,7 +137,7 @@ def validate_number_of_normal_samples(
136137

137138

138139
def validate_each_new_case_has_an_affected_sample(
139-
order: MIPDNAOrder, **kwargs
140+
order: MIPDNAOrder | NalloOrder, **kwargs
140141
) -> list[NewCaseWithoutAffectedSampleError]:
141142
"""Validates that each case in the order contains at least one sample with affected status."""
142143
errors: list[NewCaseWithoutAffectedSampleError] = []
@@ -148,7 +149,7 @@ def validate_each_new_case_has_an_affected_sample(
148149

149150

150151
def validate_existing_cases_have_an_affected_sample(
151-
order: MIPDNAOrder, store: Store, **kwargs
152+
order: MIPDNAOrder | NalloOrder, store: Store, **kwargs
152153
) -> list[ExistingCaseWithoutAffectedSampleError]:
153154
errors: list[ExistingCaseWithoutAffectedSampleError] = []
154155
for case_index, case in order.enumerated_existing_cases:

cg/services/orders/validation/rules/case_sample/rules.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
OccupiedWellError,
2424
PedigreeError,
2525
SampleDoesNotExistError,
26+
SampleNameAlreadyExistsError,
2627
SampleNameRepeatedError,
2728
SampleNameSameAsCaseNameError,
2829
SampleOutsideOfCollaborationError,
@@ -57,8 +58,8 @@
5758
is_buffer_missing,
5859
is_concentration_missing,
5960
is_container_name_missing,
60-
is_invalid_plate_well_format,
6161
is_invalid_capture_kit,
62+
is_invalid_plate_well_format,
6263
is_sample_missing_capture_kit,
6364
is_sample_not_from_collaboration,
6465
is_sample_tube_name_reused,
@@ -266,6 +267,8 @@ def validate_wells_contain_at_most_one_sample(
266267
def validate_sample_names_not_repeated(
267268
order: OrderWithCases, store: Store, **kwargs
268269
) -> list[SampleNameRepeatedError]:
270+
"""Ensures that sample names are unique within the order
271+
and that they not already used in the case previously."""
269272
old_sample_names: set[str] = get_existing_sample_names(order=order, status_db=store)
270273
new_samples: list[tuple[int, int, SampleInCase]] = order.enumerated_new_samples
271274
sample_name_counter = Counter([sample.name for _, _, sample in new_samples])
@@ -494,3 +497,16 @@ def validate_existing_samples_belong_to_collaboration(
494497
)
495498
errors.append(error)
496499
return errors
500+
501+
502+
def validate_sample_names_available(
503+
order: OrderWithCases, store: Store, **kwargs
504+
) -> list[SampleNameAlreadyExistsError]:
505+
"""Validates that new sample names are not already used by the customer."""
506+
errors: list[SampleNameAlreadyExistsError] = []
507+
customer_entry_id: int = store.get_customer_by_internal_id(order.customer).id
508+
for case_index, sample_index, sample in order.enumerated_new_samples:
509+
if store.is_sample_name_used(sample=sample, customer_entry_id=customer_entry_id):
510+
error = SampleNameAlreadyExistsError(case_index=case_index, sample_index=sample_index)
511+
errors.append(error)
512+
return errors

cg/store/crud/read.py

+8
Original file line numberDiff line numberDiff line change
@@ -1831,3 +1831,11 @@ def get_pacbio_sequencing_runs_by_run_name(self, run_name: str) -> list[PacbioSe
18311831
if runs.count() == 0:
18321832
raise EntryNotFoundError(f"Could not find any sequencing runs for {run_name}")
18331833
return runs.all()
1834+
1835+
def is_sample_name_used(self, sample: Sample, customer_entry_id: int) -> bool:
1836+
"""Check if a sample name is already used by the customer"""
1837+
if self.get_sample_by_customer_and_name(
1838+
customer_entry_id=[customer_entry_id], sample_name=sample.name
1839+
):
1840+
return True
1841+
return False

tests/apps/orderform/test_json_orderform_parser.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66

77

88
@pytest.mark.parametrize(
9-
"valid_json_order_type", [OrderType.MIP_DNA, OrderType.BALSAMIC, OrderType.FLUFFY]
9+
"valid_json_order_type",
10+
[OrderType.MIP_DNA, OrderType.BALSAMIC, OrderType.FLUFFY],
1011
)
11-
def test_generate_mip_json_orderform(valid_json_order_type: str, json_order_dict: dict):
12+
def test_generate_json_orderform(valid_json_order_type: str, json_order_dict: dict):
1213
"""Tests the orderform generation for customer-submitted json files"""
1314

1415
# GIVEN a dictionary from a JSON file of a certain order type

0 commit comments

Comments
 (0)