grz-cli 0.1.3__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {grz_cli-0.1.3/src/grz_cli.egg-info → grz_cli-0.1.4}/PKG-INFO +3 -4
- {grz_cli-0.1.3 → grz_cli-0.1.4}/pyproject.toml +11 -3
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/download.py +1 -1
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/models/v1_0_0/metadata.py +124 -45
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/parser.py +7 -1
- grz_cli-0.1.4/src/grz_cli/resources/grz-schema.json +595 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/upload.py +1 -1
- {grz_cli-0.1.3 → grz_cli-0.1.4/src/grz_cli.egg-info}/PKG-INFO +3 -4
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli.egg-info/requires.txt +1 -1
- {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_cli.py +16 -16
- {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_download.py +3 -3
- {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_parser.py +6 -4
- {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_upload.py +20 -20
- grz_cli-0.1.3/src/grz_cli/resources/grz-schema.json +0 -574
- {grz_cli-0.1.3 → grz_cli-0.1.4}/LICENSE +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/MANIFEST.in +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/README.md +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/setup.cfg +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/__init__.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/__init__.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/cli.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/constants.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/fastq_validation.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/file_operations.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/logging_setup.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/models/__init__.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/models/config.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/models/v1_0_0/__init__.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/progress_logging.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/resources/thresholds.json +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli/states.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli.egg-info/SOURCES.txt +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli.egg-info/dependency_links.txt +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli.egg-info/entry_points.txt +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/src/grz_cli.egg-info/top_level.txt +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_fastq_validation.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_file_operations.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_file_progress_logging.py +0 -0
- {grz_cli-0.1.3 → grz_cli-0.1.4}/tests/test_read_multiple_json.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: grz-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Tool for validation, encryption and upload of MV submissions to GDCs.
|
|
5
5
|
Author-email: Koray Kirli <koraykirli@gmail.com>, Mathias Lesche <mathias.lesche@tu-dresden.de>, "Florian R. Hölzlwimmer" <git.ich@frhoelzlwimmer.de>, Till Hartmann <till.hartmann@bih-charite.de>, Thomas Sell <thomas.sell@bih-charite.de>
|
|
6
6
|
License: MIT
|
|
@@ -11,7 +11,6 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Requires-Python: >=3.12
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
|
-
License-File: LICENSE
|
|
15
14
|
Requires-Dist: boto3<2,>=1.35.39
|
|
16
15
|
Requires-Dist: click<9,>=8.1.7
|
|
17
16
|
Requires-Dist: crypt4gh<2,>=1.7
|
|
@@ -26,7 +25,7 @@ Requires-Dist: pytest-cov<6,>=5.0.0; extra == "test"
|
|
|
26
25
|
Requires-Dist: pytest-mock<4,>=3.14.0; extra == "test"
|
|
27
26
|
Requires-Dist: moto[s3]<6,>=5.0.16; extra == "test"
|
|
28
27
|
Requires-Dist: pytest-sugar<2,>=1.0.0; extra == "test"
|
|
29
|
-
Requires-Dist: ruff<1,>=0.
|
|
28
|
+
Requires-Dist: ruff<1,>=0.9.6; extra == "test"
|
|
30
29
|
Requires-Dist: numpy<3,>=2.1.2; extra == "test"
|
|
31
30
|
|
|
32
31
|
# GRZ CLI
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "grz-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Tool for validation, encryption and upload of MV submissions to GDCs."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -46,7 +46,7 @@ test = [
|
|
|
46
46
|
"pytest-mock >=3.14.0,<4",
|
|
47
47
|
"moto[s3] >=5.0.16,<6",
|
|
48
48
|
"pytest-sugar >=1.0.0,<2",
|
|
49
|
-
"ruff >=0.
|
|
49
|
+
"ruff >=0.9.6,<1",
|
|
50
50
|
"numpy >=2.1.2,<3"
|
|
51
51
|
]
|
|
52
52
|
|
|
@@ -154,21 +154,25 @@ allowlist_externals = ["pytest", "ruff"]
|
|
|
154
154
|
isolated_build = true
|
|
155
155
|
|
|
156
156
|
[tool.tox.env_run_base]
|
|
157
|
+
runner = "uv-venv-lock-runner"
|
|
157
158
|
description = "run unit tests"
|
|
158
159
|
dependency_groups = ["test"]
|
|
159
160
|
commands = [["pytest", { replace = "posargs", default = ["tests"], extend = true }]]
|
|
160
161
|
|
|
161
162
|
[tool.tox.env."format-check"]
|
|
163
|
+
runner = "uv-venv-lock-runner"
|
|
162
164
|
skip_install = true
|
|
163
165
|
dependency_groups = ["lint"]
|
|
164
166
|
commands = [["ruff", "format", "--check"]]
|
|
165
167
|
|
|
166
168
|
[tool.tox.env."lints"]
|
|
169
|
+
runner = "uv-venv-lock-runner"
|
|
167
170
|
skip_install = true
|
|
168
171
|
dependency_groups = ["lint"]
|
|
169
172
|
commands = [["ruff", "check"]]
|
|
170
173
|
|
|
171
174
|
[tool.tox.env."mypy"]
|
|
175
|
+
runner = "uv-venv-lock-runner"
|
|
172
176
|
skip_install = false
|
|
173
177
|
dependency_groups = ["lint"]
|
|
174
178
|
commands = [["mypy"]]
|
|
@@ -177,8 +181,9 @@ commands = [["mypy"]]
|
|
|
177
181
|
dev-dependencies = [
|
|
178
182
|
"datamodel-code-generator>=0.26.2",
|
|
179
183
|
"mypy>=1.13.0",
|
|
180
|
-
"ruff>=0.
|
|
184
|
+
"ruff>=0.9.6",
|
|
181
185
|
"tox>=4.23.2",
|
|
186
|
+
"tox-uv>=1.20.1",
|
|
182
187
|
"types-pyyaml>=6.0.12.20240917",
|
|
183
188
|
"types-tqdm>=4.66.0.20240417",
|
|
184
189
|
]
|
|
@@ -192,3 +197,6 @@ packages = "src"
|
|
|
192
197
|
addopts = [
|
|
193
198
|
"--import-mode=importlib",
|
|
194
199
|
]
|
|
200
|
+
|
|
201
|
+
[tool.setuptools]
|
|
202
|
+
license-files = []
|
|
@@ -146,7 +146,7 @@ class S3BotoDownloadWorker:
|
|
|
146
146
|
if filesize / MULTIPART_CHUNKSIZE > MULTIPART_MAX_CHUNKS
|
|
147
147
|
else MULTIPART_CHUNKSIZE
|
|
148
148
|
)
|
|
149
|
-
self.__log.debug(f"Using a chunksize of: {chunksize / 1024**2}MiB, results in {filesize/chunksize} chunks")
|
|
149
|
+
self.__log.debug(f"Using a chunksize of: {chunksize / 1024**2}MiB, results in {filesize / chunksize} chunks")
|
|
150
150
|
|
|
151
151
|
config = TransferConfig(
|
|
152
152
|
multipart_threshold=MULTIPART_THRESHOLD,
|
|
@@ -18,6 +18,7 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
import json
|
|
20
20
|
import logging
|
|
21
|
+
import typing
|
|
21
22
|
from collections.abc import Generator
|
|
22
23
|
from datetime import date
|
|
23
24
|
from enum import StrEnum
|
|
@@ -90,9 +91,9 @@ class Submission(StrictBaseModel):
|
|
|
90
91
|
The options are: 'initial' for first submission, 'followup' is for followup submissions, 'addition' for additional submission, 'correction' for correction
|
|
91
92
|
"""
|
|
92
93
|
|
|
93
|
-
tan_g: Annotated[str, StringConstraints(pattern=r"^[a-fA-F0-9]{
|
|
94
|
+
tan_g: Annotated[str, StringConstraints(pattern=r"^[a-fA-F0-9]{64}$")]
|
|
94
95
|
"""
|
|
95
|
-
The VNg of the genomic data of the patient that will be reimbursed --> a unique 32-byte hex
|
|
96
|
+
The VNg of the genomic data of the patient that will be reimbursed --> a unique 32-length byte code represented in a hex string of length 64.
|
|
96
97
|
"""
|
|
97
98
|
|
|
98
99
|
local_case_id: str | None = None
|
|
@@ -134,9 +135,9 @@ class Submission(StrictBaseModel):
|
|
|
134
135
|
"""
|
|
135
136
|
|
|
136
137
|
|
|
137
|
-
class
|
|
138
|
+
class Gender(StrEnum):
|
|
138
139
|
"""
|
|
139
|
-
|
|
140
|
+
Gender of the donor.
|
|
140
141
|
"""
|
|
141
142
|
|
|
142
143
|
male = "male"
|
|
@@ -156,6 +157,7 @@ class Relation(StrEnum):
|
|
|
156
157
|
sister = "sister"
|
|
157
158
|
child = "child"
|
|
158
159
|
self = "self"
|
|
160
|
+
other = "other"
|
|
159
161
|
|
|
160
162
|
|
|
161
163
|
class MvConsentScope(StrictBaseModel):
|
|
@@ -284,6 +286,7 @@ class EnrichmentKitManufacturer(StrEnum):
|
|
|
284
286
|
neb = "NEB"
|
|
285
287
|
other = "other"
|
|
286
288
|
unknown = "unknown"
|
|
289
|
+
none = "none"
|
|
287
290
|
|
|
288
291
|
|
|
289
292
|
class SequencingLayout(StrEnum):
|
|
@@ -308,6 +311,22 @@ class TumorCellCountMethod(StrEnum):
|
|
|
308
311
|
unknown = "unknown"
|
|
309
312
|
|
|
310
313
|
|
|
314
|
+
class TumorCellCount(StrictBaseModel):
|
|
315
|
+
"""
|
|
316
|
+
Tuple of tumor cell counts and how they were determined.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
count: Annotated[float, Field(ge=0.0, le=100.0)]
|
|
320
|
+
"""
|
|
321
|
+
Tumor cell count in %
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
method: TumorCellCountMethod
|
|
325
|
+
"""
|
|
326
|
+
Method used to determine cell count.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
|
|
311
330
|
class CallerUsedItem(StrictBaseModel):
|
|
312
331
|
name: str
|
|
313
332
|
"""
|
|
@@ -443,7 +462,9 @@ class PercentBasesAboveQualityThreshold(StrictBaseModel):
|
|
|
443
462
|
"""The minimum quality score threshold"""
|
|
444
463
|
|
|
445
464
|
percent: Annotated[float, Field(strict=True, ge=0.0, le=100.0)]
|
|
446
|
-
"""
|
|
465
|
+
"""
|
|
466
|
+
Percentage of bases with a specified minimum quality threshold, according to https://www.bfarm.de/SharedDocs/Downloads/DE/Forschung/modellvorhaben-genomsequenzierung/Qs-durch-GRZ.pdf?__blob=publicationFile
|
|
467
|
+
"""
|
|
447
468
|
|
|
448
469
|
|
|
449
470
|
class SequenceData(StrictBaseModel):
|
|
@@ -502,6 +523,12 @@ class SequenceData(StrictBaseModel):
|
|
|
502
523
|
List of files generated and required in this analysis.
|
|
503
524
|
"""
|
|
504
525
|
|
|
526
|
+
def contains_files(self, file_type: FileType) -> bool:
|
|
527
|
+
return any(f.file_type == file_type for f in self.files)
|
|
528
|
+
|
|
529
|
+
def list_files(self, file_type: FileType) -> list[File]:
|
|
530
|
+
return [f for f in self.files if f.file_type == file_type]
|
|
531
|
+
|
|
505
532
|
|
|
506
533
|
class LabDatum(StrictBaseModel):
|
|
507
534
|
lab_data_name: str
|
|
@@ -601,21 +628,19 @@ class LabDatum(StrictBaseModel):
|
|
|
601
628
|
The sequencing layout, aka the end type of sequencing.
|
|
602
629
|
"""
|
|
603
630
|
|
|
604
|
-
tumor_cell_count:
|
|
605
|
-
"""
|
|
606
|
-
Tumor cell count in %
|
|
607
|
-
"""
|
|
608
|
-
|
|
609
|
-
tumor_cell_count_method: TumorCellCountMethod | None = None
|
|
631
|
+
tumor_cell_count: list[TumorCellCount] | None = None
|
|
610
632
|
"""
|
|
611
|
-
|
|
633
|
+
Tuple of tumor cell counts and how they were determined.
|
|
612
634
|
"""
|
|
613
635
|
|
|
614
|
-
sequence_data: SequenceData
|
|
636
|
+
sequence_data: SequenceData | None = None
|
|
615
637
|
"""
|
|
616
638
|
Sequence data generated from the wet lab experiment.
|
|
617
639
|
"""
|
|
618
640
|
|
|
641
|
+
def has_sequence_data(self) -> bool:
|
|
642
|
+
return self.sequence_data is not None
|
|
643
|
+
|
|
619
644
|
@model_validator(mode="after")
|
|
620
645
|
def validate_sequencing_setup(self) -> Self:
|
|
621
646
|
if self.library_type in {LibraryType.wxs, LibraryType.wxs_lr} and self.sequence_type != SequenceType.rna:
|
|
@@ -627,14 +652,19 @@ class LabDatum(StrictBaseModel):
|
|
|
627
652
|
|
|
628
653
|
|
|
629
654
|
class Donor(StrictBaseModel):
|
|
630
|
-
|
|
655
|
+
tan_g: Annotated[str, StringConstraints(pattern=r"^[a-fA-F0-9]{64}$")]
|
|
631
656
|
"""
|
|
632
|
-
The VNg of the genomic data of the donor --> a unique 32-byte hex
|
|
657
|
+
The VNg of the genomic data of the donor --> a unique 32-length byte code represented in a hex string of length 64.
|
|
633
658
|
"""
|
|
634
659
|
|
|
635
|
-
|
|
660
|
+
local_case_id: str | None = None
|
|
636
661
|
"""
|
|
637
|
-
|
|
662
|
+
A local case identifier for synchronizing locally
|
|
663
|
+
"""
|
|
664
|
+
|
|
665
|
+
gender: Gender
|
|
666
|
+
"""
|
|
667
|
+
Gender of the donor.
|
|
638
668
|
"""
|
|
639
669
|
|
|
640
670
|
relation: Relation
|
|
@@ -707,15 +737,21 @@ class Donor(StrictBaseModel):
|
|
|
707
737
|
Lab data related to the donor.
|
|
708
738
|
"""
|
|
709
739
|
|
|
740
|
+
@model_validator(mode="after")
|
|
741
|
+
def warn_empty_sequence_data(self):
|
|
742
|
+
for lab_datum in self.lab_data:
|
|
743
|
+
if not lab_datum.has_sequence_data():
|
|
744
|
+
log.warning(
|
|
745
|
+
f"No sequence data found for lab datum '{lab_datum.lab_data_name}' in donor '{self.tan_g}'. "
|
|
746
|
+
"Is this a submission without sequence data?"
|
|
747
|
+
)
|
|
748
|
+
return self
|
|
749
|
+
|
|
710
750
|
@model_validator(mode="after")
|
|
711
751
|
def validate_target_bed_files_exist(self):
|
|
712
752
|
"""
|
|
713
753
|
Check if the submission has the required bed files for panel sequencing.
|
|
714
754
|
"""
|
|
715
|
-
|
|
716
|
-
def contains_bed_files(sequence_data: SequenceData) -> bool:
|
|
717
|
-
return any(f.file_type == FileType.bed for f in sequence_data.files)
|
|
718
|
-
|
|
719
755
|
lib_types = {
|
|
720
756
|
LibraryType.panel,
|
|
721
757
|
LibraryType.wes,
|
|
@@ -726,41 +762,36 @@ class Donor(StrictBaseModel):
|
|
|
726
762
|
}
|
|
727
763
|
|
|
728
764
|
for lab_datum in self.lab_data:
|
|
729
|
-
if
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
)
|
|
765
|
+
if (
|
|
766
|
+
lab_datum.has_sequence_data()
|
|
767
|
+
and lab_datum.library_type in lib_types
|
|
768
|
+
and not lab_datum.sequence_data.contains_files(FileType.bed)
|
|
769
|
+
):
|
|
770
|
+
raise ValueError(f"BED file missing for lab datum '{lab_datum.lab_data_name}' in donor '{self.tan_g}'.")
|
|
733
771
|
|
|
734
772
|
return self
|
|
735
773
|
|
|
736
774
|
@model_validator(mode="after")
|
|
737
775
|
def validate_vcf_file_exists(self):
|
|
738
776
|
"""
|
|
739
|
-
|
|
777
|
+
Check if there is a VCF file
|
|
740
778
|
"""
|
|
741
|
-
|
|
742
|
-
def contains_vcf_files(sequence_data: SequenceData) -> bool:
|
|
743
|
-
return any(f.file_type == FileType.vcf for f in sequence_data.files)
|
|
744
|
-
|
|
745
779
|
for lab_datum in self.lab_data:
|
|
746
|
-
if not
|
|
747
|
-
raise ValueError(
|
|
748
|
-
f"VCF file missing for lab datum '{lab_datum.lab_data_name}' in donor '{self.case_id}'."
|
|
749
|
-
)
|
|
780
|
+
if lab_datum.has_sequence_data() and not lab_datum.sequence_data.contains_files(FileType.vcf):
|
|
781
|
+
raise ValueError(f"VCF file missing for lab datum '{lab_datum.lab_data_name}' in donor '{self.tan_g}'.")
|
|
750
782
|
|
|
751
783
|
return self
|
|
752
784
|
|
|
753
785
|
@model_validator(mode="after")
|
|
754
786
|
def validate_fastq_file_exists(self):
|
|
755
787
|
"""
|
|
756
|
-
|
|
788
|
+
Check if there is a FASTQ file
|
|
757
789
|
"""
|
|
758
|
-
|
|
759
|
-
def get_fastq_files(sequence_data: SequenceData):
|
|
760
|
-
return [f for f in sequence_data.files if f.file_type == FileType.fastq]
|
|
761
|
-
|
|
762
790
|
for lab_datum in self.lab_data:
|
|
763
|
-
|
|
791
|
+
if not lab_datum.has_sequence_data():
|
|
792
|
+
# Skip if no sequence data is present
|
|
793
|
+
continue
|
|
794
|
+
fastq_files = lab_datum.sequence_data.list_files(FileType.fastq)
|
|
764
795
|
|
|
765
796
|
if len(fastq_files) == 0:
|
|
766
797
|
raise ValueError("No FASTQ file found!")
|
|
@@ -769,7 +800,7 @@ class Donor(StrictBaseModel):
|
|
|
769
800
|
for i in fastq_files:
|
|
770
801
|
if i.read_order is None:
|
|
771
802
|
raise ValueError(
|
|
772
|
-
f"Error in lab datum '{lab_datum.lab_data_name}' of donor '{self.
|
|
803
|
+
f"Error in lab datum '{lab_datum.lab_data_name}' of donor '{self.tan_g}': "
|
|
773
804
|
f"No read order specified for FASTQ file '{i.file_path}'!"
|
|
774
805
|
)
|
|
775
806
|
|
|
@@ -779,7 +810,7 @@ class Donor(StrictBaseModel):
|
|
|
779
810
|
|
|
780
811
|
if len(r1_fastq_files) != len(r2_fastq_files):
|
|
781
812
|
raise ValueError(
|
|
782
|
-
f"Error in lab datum '{lab_datum.lab_data_name}' of donor '{self.
|
|
813
|
+
f"Error in lab datum '{lab_datum.lab_data_name}' of donor '{self.tan_g}': "
|
|
783
814
|
f"Paired end sequencing layout but number of R1 FASTQ files ({len(r1_fastq_files)})"
|
|
784
815
|
f" differs from number of R2 FASTQ files ({len(r2_fastq_files)})!"
|
|
785
816
|
)
|
|
@@ -821,6 +852,37 @@ class GrzSubmissionMetadata(StrictBaseModel):
|
|
|
821
852
|
|
|
822
853
|
return self
|
|
823
854
|
|
|
855
|
+
@model_validator(mode="after")
|
|
856
|
+
def check_for_tumor_cell_count(self):
|
|
857
|
+
"""
|
|
858
|
+
Check if oncology samples have tumor cell counts.
|
|
859
|
+
"""
|
|
860
|
+
for donor in self.donors:
|
|
861
|
+
case_id = donor.tan_g
|
|
862
|
+
for lab_datum in donor.lab_data:
|
|
863
|
+
if lab_datum.sequence_subtype == SequenceSubtype.somatic and lab_datum.tumor_cell_count is None:
|
|
864
|
+
raise ValueError(
|
|
865
|
+
f"Missing tumor cell count for donor '{case_id}', lab datum '{lab_datum.lab_data_name}'!"
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
return self
|
|
869
|
+
|
|
870
|
+
@model_validator(mode="after")
|
|
871
|
+
def check_duplicate_lab_data_names(self):
|
|
872
|
+
"""
|
|
873
|
+
Check if the submission contains lab data with the same name within one donor.
|
|
874
|
+
"""
|
|
875
|
+
for donor in self.donors:
|
|
876
|
+
case_id = donor.tan_g
|
|
877
|
+
lab_data_names = set()
|
|
878
|
+
for lab_datum in donor.lab_data:
|
|
879
|
+
if lab_datum.lab_data_name in lab_data_names:
|
|
880
|
+
raise ValueError(f"Duplicate lab datum '{lab_datum.lab_data_name}' in donor '{case_id}'")
|
|
881
|
+
else:
|
|
882
|
+
lab_data_names.add(lab_datum.lab_data_name)
|
|
883
|
+
|
|
884
|
+
return self
|
|
885
|
+
|
|
824
886
|
@model_validator(mode="after")
|
|
825
887
|
def validate_thresholds(self):
|
|
826
888
|
"""
|
|
@@ -837,7 +899,20 @@ class GrzSubmissionMetadata(StrictBaseModel):
|
|
|
837
899
|
)
|
|
838
900
|
thresholds = threshold_definitions.get(key)
|
|
839
901
|
if thresholds is None:
|
|
840
|
-
|
|
902
|
+
allowed_combinations = sorted(list(threshold_definitions.keys()))
|
|
903
|
+
allowed_combinations = "\n".join([f" - {combination}" for combination in allowed_combinations])
|
|
904
|
+
names = (
|
|
905
|
+
"submission.genomicStudySubtype",
|
|
906
|
+
"labData.libraryType",
|
|
907
|
+
"labData.sequenceSubtype",
|
|
908
|
+
)
|
|
909
|
+
info = dict(zip(names, key, strict=True))
|
|
910
|
+
log.warning(
|
|
911
|
+
f"No thresholds for the specified combination {info} found (donor {donor.tan_g})!\n"
|
|
912
|
+
f"Valid combinations:\n{allowed_combinations}.\n"
|
|
913
|
+
f"See https://www.bfarm.de/SharedDocs/Downloads/DE/Forschung/modellvorhaben-genomsequenzierung/Qs-durch-GRZ.pdf?__blob=publicationFile for more details.\n"
|
|
914
|
+
f"Skipping threshold validation."
|
|
915
|
+
)
|
|
841
916
|
continue
|
|
842
917
|
|
|
843
918
|
_check_thresholds(donor, lab_datum, thresholds)
|
|
@@ -846,9 +921,13 @@ class GrzSubmissionMetadata(StrictBaseModel):
|
|
|
846
921
|
|
|
847
922
|
|
|
848
923
|
def _check_thresholds(donor: Donor, lab_datum: LabDatum, thresholds: dict[str, Any]):
|
|
849
|
-
|
|
850
|
-
|
|
924
|
+
if not lab_datum.has_sequence_data():
|
|
925
|
+
# Skip if no sequence data is present; warning issues in the validator `warn_empty_sequence_data` of `Donor`.
|
|
926
|
+
return
|
|
927
|
+
case_id = donor.tan_g
|
|
851
928
|
lab_data_name = lab_datum.lab_data_name
|
|
929
|
+
# mypy cannot reason about the `has_sequence_data` check
|
|
930
|
+
sequence_data = typing.cast(SequenceData, lab_datum.sequence_data)
|
|
852
931
|
|
|
853
932
|
mean_depth_of_coverage_t = thresholds.get("meanDepthOfCoverage")
|
|
854
933
|
mean_depth_of_coverage_v = sequence_data.mean_depth_of_coverage
|
|
@@ -9,6 +9,8 @@ from itertools import groupby
|
|
|
9
9
|
from os import PathLike
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
+
from pydantic import ValidationError
|
|
13
|
+
|
|
12
14
|
from .download import S3BotoDownloadWorker
|
|
13
15
|
from .fastq_validation import validate_paired_end_reads, validate_single_end_reads
|
|
14
16
|
from .file_operations import Crypt4GH, calculate_sha256
|
|
@@ -60,7 +62,11 @@ class SubmissionMetadata:
|
|
|
60
62
|
try:
|
|
61
63
|
with open(file_path, encoding="utf-8") as jsonfile:
|
|
62
64
|
metadata = json.load(jsonfile)
|
|
63
|
-
|
|
65
|
+
try:
|
|
66
|
+
metadata_model = GrzSubmissionMetadata(**metadata)
|
|
67
|
+
except ValidationError as ve:
|
|
68
|
+
cls.__log.error("Invalid metadata format in metadata file: %s", file_path)
|
|
69
|
+
raise SystemExit(ve) from ve
|
|
64
70
|
return metadata_model
|
|
65
71
|
except json.JSONDecodeError as e:
|
|
66
72
|
cls.__log.error("Invalid JSON format in metadata file: %s", file_path)
|