grz-cli 0.1.0__py3-none-any.whl
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.
- __init__.py +0 -0
- grz_cli/__init__.py +0 -0
- grz_cli/cli.py +326 -0
- grz_cli/constants.py +37 -0
- grz_cli/download.py +236 -0
- grz_cli/fastq_validation.py +122 -0
- grz_cli/file_operations.py +364 -0
- grz_cli/logging_setup.py +59 -0
- grz_cli/models/__init__.py +0 -0
- grz_cli/models/config.py +118 -0
- grz_cli/models/v1_0_0/__init__.py +0 -0
- grz_cli/models/v1_0_0/metadata.py +845 -0
- grz_cli/parser.py +771 -0
- grz_cli/progress_logging.py +170 -0
- grz_cli/resources/grz-schema.json +560 -0
- grz_cli/upload.py +243 -0
- grz_cli-0.1.0.dist-info/LICENSE +21 -0
- grz_cli-0.1.0.dist-info/METADATA +240 -0
- grz_cli-0.1.0.dist-info/RECORD +22 -0
- grz_cli-0.1.0.dist-info/WHEEL +5 -0
- grz_cli-0.1.0.dist-info/entry_points.txt +2 -0
- grz_cli-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
# generated by datamodel-codegen:
|
|
2
|
+
# filename: grz-schema.json
|
|
3
|
+
# timestamp: 2024-11-21T12:40:39+00:00
|
|
4
|
+
# version: 0.26.2
|
|
5
|
+
### Command run:
|
|
6
|
+
## datamodel-codegen --output-model-type pydantic_v2.BaseModel \
|
|
7
|
+
## --input src/grz_cli/resources/grz-schema.json \
|
|
8
|
+
## --use-schema-description --use-field-description --snake-case-field --use-standard-collections --use-union-operator \
|
|
9
|
+
## --field-constraints --use-annotated --enable-version-header --reuse-model --use-subclass-enum --union-mode smart \
|
|
10
|
+
## --class-name 'GrzSubmissionMetadata' --use-double-quotes --target-python-version 3.12
|
|
11
|
+
### Custom modifications:
|
|
12
|
+
### - disallow extra fields (no additional properties allowed)
|
|
13
|
+
### - validate_assignment (i.e. when setting properties, re-trigger validations)
|
|
14
|
+
### - use_enum_values (use values [strings] instead of enum variants)
|
|
15
|
+
### - remove those aliases which are covered by a global `alias_generator=to_camel` setting
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Generator
|
|
20
|
+
from datetime import date
|
|
21
|
+
from enum import StrEnum
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Annotated, Any
|
|
24
|
+
|
|
25
|
+
from pydantic import (
|
|
26
|
+
BaseModel,
|
|
27
|
+
ConfigDict,
|
|
28
|
+
Field,
|
|
29
|
+
StringConstraints,
|
|
30
|
+
model_validator,
|
|
31
|
+
)
|
|
32
|
+
from pydantic.alias_generators import to_camel
|
|
33
|
+
|
|
34
|
+
from grz_cli.file_operations import calculate_sha256 # type: ignore
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class StrictBaseModel(BaseModel):
|
|
38
|
+
model_config = ConfigDict(
|
|
39
|
+
extra="forbid",
|
|
40
|
+
validate_assignment=True,
|
|
41
|
+
use_enum_values=True,
|
|
42
|
+
alias_generator=to_camel,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SubmissionType(StrEnum):
|
|
47
|
+
"""
|
|
48
|
+
The options are: 'initial' for first submission, 'followup' is for followup submissions, 'addition' for additional submission, 'correction' for correction
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
initial = "initial"
|
|
52
|
+
followup = "followup"
|
|
53
|
+
addition = "addition"
|
|
54
|
+
correction = "correction"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GenomicStudyType(StrEnum):
|
|
58
|
+
"""
|
|
59
|
+
whether additional persons are tested as well
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
single = "single"
|
|
63
|
+
duo = "duo"
|
|
64
|
+
trio = "trio"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class GenomicStudySubtype(StrEnum):
|
|
68
|
+
"""
|
|
69
|
+
whether tumor and/or germ-line are tested
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
tumor_only = "tumor-only"
|
|
73
|
+
tumor_germline = "tumor+germline"
|
|
74
|
+
germline_only = "germline-only"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Submission(StrictBaseModel):
|
|
78
|
+
submission_date: date
|
|
79
|
+
"""
|
|
80
|
+
Date of submission in ISO 8601 format YYYY-MM-DD
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
submission_type: SubmissionType
|
|
84
|
+
"""
|
|
85
|
+
The options are: 'initial' for first submission, 'followup' is for followup submissions, 'addition' for additional submission, 'correction' for correction
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
tan_g: Annotated[str, StringConstraints(pattern=r"^[a-fA-F0-9]{32}$")]
|
|
89
|
+
"""
|
|
90
|
+
The VNg of the genomic data of the patient that will be reimbursed --> a unique 32-byte hex ID.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
local_case_id: str | None = None
|
|
94
|
+
"""
|
|
95
|
+
A local case identifier for synchronizing locally
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
submitter_id: Annotated[str, StringConstraints(pattern=r"^[0-9]{9}$")]
|
|
99
|
+
"""
|
|
100
|
+
Institutional ID of the submitter according to §293 SGB V.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
genomic_data_center_id: Annotated[
|
|
104
|
+
str,
|
|
105
|
+
StringConstraints(pattern=r"^(GRZ|KDK)[A-Z0-9]{3}[0-9]{3}$"),
|
|
106
|
+
]
|
|
107
|
+
"""
|
|
108
|
+
ID of the genomic data center in the format GRZXXXnnn.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
clinical_data_node_id: Annotated[
|
|
112
|
+
str, StringConstraints(pattern=r"^(GRZ|KDK)[A-Z0-9]{3}[0-9]{3}$")
|
|
113
|
+
]
|
|
114
|
+
"""
|
|
115
|
+
ID of the genomic data center in the format KDKXXXnnn.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
genomic_study_type: GenomicStudyType
|
|
119
|
+
"""
|
|
120
|
+
whether additional persons are tested as well
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
genomic_study_subtype: GenomicStudySubtype
|
|
124
|
+
"""
|
|
125
|
+
whether tumor and/or germ-line are tested
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
lab_name: str
|
|
129
|
+
"""
|
|
130
|
+
Name of the sequencing lab.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Sex(StrEnum):
|
|
135
|
+
"""
|
|
136
|
+
Sex of the donor.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
male = "male"
|
|
140
|
+
female = "female"
|
|
141
|
+
other = "other"
|
|
142
|
+
unknown = "unknown"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Relation(StrEnum):
|
|
146
|
+
"""
|
|
147
|
+
Relationship of the donor in respect to the index patient, e.g. 'index', 'brother', 'mother', etc.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
mother = "mother"
|
|
151
|
+
father = "father"
|
|
152
|
+
brother = "brother"
|
|
153
|
+
sister = "sister"
|
|
154
|
+
child = "child"
|
|
155
|
+
self = "self"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class MvConsentScope(StrictBaseModel):
|
|
159
|
+
"""
|
|
160
|
+
The scope of the Modellvorhaben consent given by the donor.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
consent_mv_sequencing: bool
|
|
164
|
+
"""
|
|
165
|
+
The donor's consent to participate in the Modellvorhaben and sequencing.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
consent_case_identification: bool
|
|
169
|
+
"""
|
|
170
|
+
The donor's consent to identify the case.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
consent_re_identification: bool
|
|
174
|
+
"""
|
|
175
|
+
The donor's consent to be re-identified and to be re-contacted in case of new findings.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class MvConsentTerminationScope(StrictBaseModel):
|
|
180
|
+
"""
|
|
181
|
+
The scope of the termination of the Modellvorhaben consent.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
consent_mv_sequencing_termination: bool | None = None
|
|
185
|
+
"""
|
|
186
|
+
The patient's termination of the Modellvorhaben consent to participate in the Modellvorhaben and sequencing
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
consent_case_identification_termination: bool | None = None
|
|
190
|
+
"""
|
|
191
|
+
The patient's termination of the Modellvorhaben consent to identify the case
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
consent_re_identification_termination: bool | None = None
|
|
195
|
+
"""
|
|
196
|
+
The patient's termination of the Modellvorhaben consent to re-identify the case and to re-contact the patient in case of new findings
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class TissueOntology(StrictBaseModel):
|
|
201
|
+
name: str
|
|
202
|
+
"""
|
|
203
|
+
Name of the tissue ontology
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
version: str
|
|
207
|
+
"""
|
|
208
|
+
Version of the tissue ontology
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class SampleConservation(StrEnum):
|
|
213
|
+
"""
|
|
214
|
+
Sample conservation
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
fresh_tissue = "fresh-tissue"
|
|
218
|
+
cryo_frozen = "cryo-frozen"
|
|
219
|
+
ffpe = "ffpe"
|
|
220
|
+
other = "other"
|
|
221
|
+
unknown = "unknown"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class SequenceType(StrEnum):
|
|
225
|
+
"""
|
|
226
|
+
Type of sequence (DNA or RNA)
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
dna = "dna"
|
|
230
|
+
rna = "rna"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class SequenceSubtype(StrEnum):
|
|
234
|
+
"""
|
|
235
|
+
Subtype of sequence (germline, somatic, etc.)
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
germline = "germline"
|
|
239
|
+
somatic = "somatic"
|
|
240
|
+
other = "other"
|
|
241
|
+
unknown = "unknown"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class FragmentationMethod(StrEnum):
|
|
245
|
+
"""
|
|
246
|
+
Fragmentation method
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
sonication = "sonication"
|
|
250
|
+
enzymatic = "enzymatic"
|
|
251
|
+
none = "none"
|
|
252
|
+
other = "other"
|
|
253
|
+
unknown = "unknown"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class LibraryType(StrEnum):
|
|
257
|
+
"""
|
|
258
|
+
Library type
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
panel = "panel"
|
|
262
|
+
wes = "wes"
|
|
263
|
+
wgs = "wgs"
|
|
264
|
+
wgs_lr = "wgs_lr"
|
|
265
|
+
wxs = "wxs"
|
|
266
|
+
other = "other"
|
|
267
|
+
unknown = "unknown"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class EnrichmentKitManufacturer(StrEnum):
|
|
271
|
+
"""
|
|
272
|
+
Manufacturer of the enrichment kit
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
illumina = "Illumina"
|
|
276
|
+
agilent = "Agilent"
|
|
277
|
+
twist = "Twist"
|
|
278
|
+
neb = "NEB"
|
|
279
|
+
other = "other"
|
|
280
|
+
unknown = "unknown"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class SequencingLayout(StrEnum):
|
|
284
|
+
"""
|
|
285
|
+
The sequencing layout, aka the end type of sequencing.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
single_end = "single-end"
|
|
289
|
+
paired_end = "paired-end"
|
|
290
|
+
reverse = "reverse"
|
|
291
|
+
other = "other"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class TumorCellCountMethod(StrEnum):
|
|
295
|
+
"""
|
|
296
|
+
Method used to determine cell count.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
pathology = "Pathology"
|
|
300
|
+
bioinformatics = "Bioinformatics"
|
|
301
|
+
other = "other"
|
|
302
|
+
unknown = "unknown"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class CallerUsedItem(StrictBaseModel):
|
|
306
|
+
name: str
|
|
307
|
+
"""
|
|
308
|
+
Name of the caller used
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
version: str
|
|
312
|
+
"""
|
|
313
|
+
Version of the caller used
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class FileType(StrEnum):
|
|
318
|
+
"""
|
|
319
|
+
Type of the file; if BED file is submitted, only 1 file is allowed.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
bam = "bam"
|
|
323
|
+
vcf = "vcf"
|
|
324
|
+
bed = "bed"
|
|
325
|
+
fastq = "fastq"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class ChecksumType(StrEnum):
|
|
329
|
+
"""
|
|
330
|
+
Type of checksum algorithm used
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
sha256 = "sha256"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class ReadOrder(StrEnum):
|
|
337
|
+
"""
|
|
338
|
+
Indicates the read order for paired-end reads.
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
r1 = "R1"
|
|
342
|
+
r2 = "R2"
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class File(StrictBaseModel):
|
|
346
|
+
file_path: str
|
|
347
|
+
"""
|
|
348
|
+
Path relative to the submission root, e.g.: sequencing_data/patient_001/patient_001_dna.bam
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
file_type: FileType
|
|
352
|
+
"""
|
|
353
|
+
Type of the file; if BED file is submitted, only 1 file is allowed.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
checksum_type: ChecksumType | None = ChecksumType.sha256
|
|
357
|
+
"""
|
|
358
|
+
Type of checksum algorithm used
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
file_checksum: Annotated[str, StringConstraints(pattern=r"^[a-fA-F0-9]{64}$")]
|
|
362
|
+
"""
|
|
363
|
+
checksum of the file
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
file_size_in_bytes: Annotated[int, Field(strict=True, ge=0)]
|
|
367
|
+
"""
|
|
368
|
+
Size of the file in bytes
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
read_order: ReadOrder | None = None
|
|
372
|
+
"""
|
|
373
|
+
Indicates the read order for paired-end reads.
|
|
374
|
+
"""
|
|
375
|
+
|
|
376
|
+
flowcell_id: str | None = None
|
|
377
|
+
"""
|
|
378
|
+
Indicates the flow cell.
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
lane_id: str | None = None
|
|
382
|
+
"""
|
|
383
|
+
Indicates the lane
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
def validate_data(self, local_file_path: Path) -> Generator[str]:
|
|
387
|
+
"""
|
|
388
|
+
Validates whether the provided file matches this metadata.
|
|
389
|
+
|
|
390
|
+
:param local_file_path: Path to the actual file (resolved if symlinked)
|
|
391
|
+
:return: Generator of errors
|
|
392
|
+
"""
|
|
393
|
+
# Resolve file path
|
|
394
|
+
local_file_path = local_file_path.resolve()
|
|
395
|
+
|
|
396
|
+
# Check if path exists
|
|
397
|
+
if not local_file_path.exists():
|
|
398
|
+
yield f"{str(self.file_path)} does not exist!"
|
|
399
|
+
# Return here as following tests cannot work
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
# Check if path is a file
|
|
403
|
+
if not local_file_path.is_file():
|
|
404
|
+
yield f"{str(self.file_path)} is not a file!"
|
|
405
|
+
# Return here as following tests cannot work
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
# Check if the checksum is correct
|
|
409
|
+
if self.checksum_type == "sha256":
|
|
410
|
+
calculated_checksum = calculate_sha256(local_file_path)
|
|
411
|
+
if self.file_checksum != calculated_checksum:
|
|
412
|
+
yield (
|
|
413
|
+
f"{str(self.file_path)}: Checksum mismatch! "
|
|
414
|
+
f"Expected: '{self.file_checksum}', calculated: '{calculated_checksum}'."
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
yield (
|
|
418
|
+
f"{str(self.file_path)}: Unsupported checksum type: {self.checksum_type}. "
|
|
419
|
+
f"Supported types: {[e.value for e in ChecksumType]}"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Check file size
|
|
423
|
+
if self.file_size_in_bytes != local_file_path.stat().st_size:
|
|
424
|
+
yield (
|
|
425
|
+
f"{str(self.file_path)}: File size mismatch! "
|
|
426
|
+
f"Expected: '{self.file_size_in_bytes}', observed: '{local_file_path.stat().st_size}'."
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def encrypted_file_path(self):
|
|
430
|
+
return self.file_path + ".c4gh"
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class SequenceData(StrictBaseModel):
|
|
434
|
+
bioinformatics_pipeline_name: str
|
|
435
|
+
"""
|
|
436
|
+
Name of the bioinformatics pipeline used
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
bioinformatics_pipeline_version: str
|
|
440
|
+
"""
|
|
441
|
+
Version or commit hash of the bioinformatics pipeline
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
reference_genome: str
|
|
445
|
+
"""
|
|
446
|
+
Reference genome used
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
percent_bases_q30: Annotated[float, Field(strict=True, ge=0.0, le=100.0)]
|
|
450
|
+
"""
|
|
451
|
+
Percentage of bases with Q30 or higher
|
|
452
|
+
"""
|
|
453
|
+
|
|
454
|
+
mean_depth_of_coverage: Annotated[float, Field(strict=True, ge=0.0)]
|
|
455
|
+
"""
|
|
456
|
+
Mean depth of coverage
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
min_coverage: Annotated[float, Field(strict=True, ge=0.0)]
|
|
460
|
+
"""
|
|
461
|
+
Minimum coverage
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
targeted_regions_above_min_coverage: Annotated[
|
|
465
|
+
float, Field(strict=True, ge=0.0, le=1.0)
|
|
466
|
+
]
|
|
467
|
+
"""
|
|
468
|
+
Fraction of targeted regions that are above minimum coverage
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
read_length: Annotated[int, Field(strict=True, ge=0)]
|
|
472
|
+
"""
|
|
473
|
+
The read length; in the case of long-read sequencing it is the rounded average read length.
|
|
474
|
+
"""
|
|
475
|
+
|
|
476
|
+
non_coding_variants: bool
|
|
477
|
+
"""
|
|
478
|
+
The analysis includes non-coding variants -> true or false
|
|
479
|
+
"""
|
|
480
|
+
|
|
481
|
+
caller_used: list[CallerUsedItem]
|
|
482
|
+
"""
|
|
483
|
+
Caller that is used in the pipeline
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
files: list[File]
|
|
487
|
+
"""
|
|
488
|
+
List of files generated and required in this analysis.
|
|
489
|
+
"""
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class LabDatum(StrictBaseModel):
|
|
493
|
+
lab_data_name: str
|
|
494
|
+
"""
|
|
495
|
+
Name/ID of the biospecimen e.g. 'Blut DNA normal'
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
tissue_ontology: TissueOntology
|
|
499
|
+
|
|
500
|
+
tissue_type_id: str
|
|
501
|
+
"""
|
|
502
|
+
Tissue ID according to the ontology in use.
|
|
503
|
+
"""
|
|
504
|
+
|
|
505
|
+
tissue_type_name: str
|
|
506
|
+
"""
|
|
507
|
+
Tissue name according to the ontology in use.
|
|
508
|
+
"""
|
|
509
|
+
|
|
510
|
+
sample_date: date
|
|
511
|
+
"""
|
|
512
|
+
Date of sample in ISO 8601 format YYYY-MM-DD
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
sample_conservation: SampleConservation
|
|
516
|
+
"""
|
|
517
|
+
Sample conservation
|
|
518
|
+
"""
|
|
519
|
+
|
|
520
|
+
sequence_type: SequenceType
|
|
521
|
+
"""
|
|
522
|
+
Type of sequence (DNA or RNA)
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
sequence_subtype: SequenceSubtype
|
|
526
|
+
"""
|
|
527
|
+
Subtype of sequence (germline, somatic, etc.)
|
|
528
|
+
"""
|
|
529
|
+
|
|
530
|
+
fragmentation_method: FragmentationMethod
|
|
531
|
+
"""
|
|
532
|
+
Fragmentation method
|
|
533
|
+
"""
|
|
534
|
+
|
|
535
|
+
library_type: LibraryType
|
|
536
|
+
"""
|
|
537
|
+
Library type
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
library_prep_kit: str
|
|
541
|
+
"""
|
|
542
|
+
Name/version of the library prepkit
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
library_prep_kit_manufacturer: str
|
|
546
|
+
"""
|
|
547
|
+
Library prep kit manufacturer
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
sequencer_model: str
|
|
551
|
+
"""
|
|
552
|
+
Name/version of the sequencer model
|
|
553
|
+
"""
|
|
554
|
+
|
|
555
|
+
sequencer_manufacturer: str
|
|
556
|
+
"""
|
|
557
|
+
Sequencer manufacturer
|
|
558
|
+
"""
|
|
559
|
+
|
|
560
|
+
kit_name: str
|
|
561
|
+
"""
|
|
562
|
+
Name/version of the sequencing kit
|
|
563
|
+
"""
|
|
564
|
+
|
|
565
|
+
kit_manufacturer: str
|
|
566
|
+
"""
|
|
567
|
+
Sequencing kit manufacturer
|
|
568
|
+
"""
|
|
569
|
+
|
|
570
|
+
enrichment_kit_manufacturer: EnrichmentKitManufacturer
|
|
571
|
+
"""
|
|
572
|
+
Manufacturer of the enrichment kit
|
|
573
|
+
"""
|
|
574
|
+
|
|
575
|
+
enrichment_kit_description: str
|
|
576
|
+
"""
|
|
577
|
+
Name/version of the enrichment kit
|
|
578
|
+
"""
|
|
579
|
+
|
|
580
|
+
barcode: str
|
|
581
|
+
"""
|
|
582
|
+
The barcode used or 'na'
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
sequencing_layout: SequencingLayout
|
|
586
|
+
"""
|
|
587
|
+
The sequencing layout, aka the end type of sequencing.
|
|
588
|
+
"""
|
|
589
|
+
|
|
590
|
+
tumor_cell_count: Annotated[
|
|
591
|
+
float | None, Field(alias="tumorCellCount", ge=0.0, le=100.0)
|
|
592
|
+
] = None
|
|
593
|
+
"""
|
|
594
|
+
Tumor cell count in %
|
|
595
|
+
"""
|
|
596
|
+
|
|
597
|
+
tumor_cell_count_method: TumorCellCountMethod | None = None
|
|
598
|
+
"""
|
|
599
|
+
Method used to determine cell count.
|
|
600
|
+
"""
|
|
601
|
+
|
|
602
|
+
sequence_data: SequenceData
|
|
603
|
+
"""
|
|
604
|
+
Sequence data generated from the wet lab experiment.
|
|
605
|
+
"""
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
class Donor(StrictBaseModel):
|
|
609
|
+
case_id: Annotated[str, StringConstraints(pattern=r"^[a-fA-F0-9]{32}$")]
|
|
610
|
+
"""
|
|
611
|
+
The VNg of the genomic data of the donor --> a unique 32-byte hex ID.
|
|
612
|
+
"""
|
|
613
|
+
|
|
614
|
+
sex: Sex
|
|
615
|
+
"""
|
|
616
|
+
Sex of the donor.
|
|
617
|
+
"""
|
|
618
|
+
|
|
619
|
+
relation: Relation
|
|
620
|
+
"""
|
|
621
|
+
Relationship of the donor in respect to the index patient, e.g. 'index', 'brother', 'mother', etc.
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
mv_consent_date: date
|
|
625
|
+
"""
|
|
626
|
+
Date of the signature of the Modellvorhaben consent; Date in ISO 8601 format YYYY-MM-DD
|
|
627
|
+
"""
|
|
628
|
+
|
|
629
|
+
mv_consent_presented_date: date | None = None
|
|
630
|
+
"""
|
|
631
|
+
Date of the presentation of the Modellvorhaben consent; Date in ISO 8601 format YYYY-MM-DD
|
|
632
|
+
"""
|
|
633
|
+
|
|
634
|
+
mv_consent_version: str
|
|
635
|
+
"""
|
|
636
|
+
Version of the research consent
|
|
637
|
+
"""
|
|
638
|
+
|
|
639
|
+
mv_consent_scope: MvConsentScope
|
|
640
|
+
"""
|
|
641
|
+
The scope of the Modellvorhaben consent given by the donor.
|
|
642
|
+
"""
|
|
643
|
+
|
|
644
|
+
mv_consent_termination_date: date | None = None
|
|
645
|
+
"""
|
|
646
|
+
Date of the termination of the Modellvorhaben consent; Date in ISO 8601 format YYYY-MM-DD
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
mv_consent_termination_scope: MvConsentTerminationScope | None = None
|
|
650
|
+
"""
|
|
651
|
+
The scope of the termination of the Modellvorhaben consent.
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
research_consent_date: date | None = None
|
|
655
|
+
"""
|
|
656
|
+
Date of the signature of the research consent; Date in ISO 8601 format YYYY-MM-DD
|
|
657
|
+
"""
|
|
658
|
+
|
|
659
|
+
research_consent_presented_date: date | None = None
|
|
660
|
+
"""
|
|
661
|
+
Date of the presentation of the research consent; Date in ISO 8601 format YYYY-MM-DD
|
|
662
|
+
"""
|
|
663
|
+
|
|
664
|
+
research_consent_version: str | None = None
|
|
665
|
+
"""
|
|
666
|
+
Version of the research consent
|
|
667
|
+
"""
|
|
668
|
+
|
|
669
|
+
research_consent_scope: dict[str, Any] | None = None
|
|
670
|
+
"""
|
|
671
|
+
Scope of the research consent given by the patient; shall be given in JSON format.
|
|
672
|
+
"""
|
|
673
|
+
|
|
674
|
+
research_consent_revocation_date: date | None = None
|
|
675
|
+
"""
|
|
676
|
+
Date of the revocation of the research consent; Date in ISO 8601 format YYYY-MM-DD
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
research_consent_revocation_scope: dict[str, Any] | None = None
|
|
680
|
+
"""
|
|
681
|
+
Scope of the revocation of the research consent given by the patient; shall be given in JSON format
|
|
682
|
+
"""
|
|
683
|
+
|
|
684
|
+
lab_data: list[LabDatum]
|
|
685
|
+
"""
|
|
686
|
+
Lab data related to the donor.
|
|
687
|
+
"""
|
|
688
|
+
|
|
689
|
+
@model_validator(mode="after")
|
|
690
|
+
def validate_target_bed_files_exist(self):
|
|
691
|
+
"""
|
|
692
|
+
Check if the submission has the required bed files for panel sequencing.
|
|
693
|
+
"""
|
|
694
|
+
|
|
695
|
+
def contains_bed_files(sequence_data: SequenceData) -> bool:
|
|
696
|
+
return any(f.file_type == FileType.bed for f in sequence_data.files)
|
|
697
|
+
|
|
698
|
+
lib_type = {LibraryType.panel, LibraryType.wes}
|
|
699
|
+
|
|
700
|
+
for lab_datum in self.lab_data:
|
|
701
|
+
if lab_datum.library_type in lib_type and not contains_bed_files(
|
|
702
|
+
lab_datum.sequence_data
|
|
703
|
+
):
|
|
704
|
+
raise ValueError(
|
|
705
|
+
f"BED file missing for lab datum '{lab_datum.lab_data_name}' in donor '{self.case_id}'."
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
return self
|
|
709
|
+
|
|
710
|
+
@model_validator(mode="after")
|
|
711
|
+
def validate_vcf_file_exists(self):
|
|
712
|
+
"""
|
|
713
|
+
check if there is a VCF file
|
|
714
|
+
"""
|
|
715
|
+
|
|
716
|
+
def contains_vcf_files(sequence_data: SequenceData) -> bool:
|
|
717
|
+
return any(f.file_type == FileType.vcf for f in sequence_data.files)
|
|
718
|
+
|
|
719
|
+
for lab_datum in self.lab_data:
|
|
720
|
+
if not contains_vcf_files(lab_datum.sequence_data):
|
|
721
|
+
raise ValueError(
|
|
722
|
+
f"VCF file missing for lab datum '{lab_datum.lab_data_name}' in donor '{self.case_id}'."
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
return self
|
|
726
|
+
|
|
727
|
+
@model_validator(mode="after")
|
|
728
|
+
def validate_fastq_file_exists(self):
|
|
729
|
+
"""
|
|
730
|
+
check if there is a FASTQ file
|
|
731
|
+
"""
|
|
732
|
+
|
|
733
|
+
def get_fastq_files(sequence_data: SequenceData):
|
|
734
|
+
return [f for f in sequence_data.files if f.file_type == FileType.fastq]
|
|
735
|
+
|
|
736
|
+
for lab_datum in self.lab_data:
|
|
737
|
+
fastq_files = get_fastq_files(lab_datum.sequence_data)
|
|
738
|
+
|
|
739
|
+
if len(fastq_files) == 0:
|
|
740
|
+
raise ValueError("No FASTQ file found!")
|
|
741
|
+
elif lab_datum.sequencing_layout == SequencingLayout.paired_end:
|
|
742
|
+
# check if read order is specified
|
|
743
|
+
for i in fastq_files:
|
|
744
|
+
if i.read_order is None:
|
|
745
|
+
raise ValueError(
|
|
746
|
+
f"Error in lab datum '{lab_datum.lab_data_name}' of donor '{self.case_id}': "
|
|
747
|
+
f"No read order specified for FASTQ file '{i.file_path}'!"
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
# check if there is an equal number of R1 and R2 files
|
|
751
|
+
r1_fastq_files = [
|
|
752
|
+
i for i in fastq_files if i.read_order == ReadOrder.r1
|
|
753
|
+
]
|
|
754
|
+
r2_fastq_files = [
|
|
755
|
+
i for i in fastq_files if i.read_order == ReadOrder.r2
|
|
756
|
+
]
|
|
757
|
+
|
|
758
|
+
if len(r1_fastq_files) != len(r2_fastq_files):
|
|
759
|
+
raise ValueError(
|
|
760
|
+
f"Error in lab datum '{lab_datum.lab_data_name}' of donor '{self.case_id}': "
|
|
761
|
+
f"Paired end sequencing layout but number of R1 FASTQ files ({len(r1_fastq_files)})"
|
|
762
|
+
f" differs from number of R2 FASTQ files ({len(r2_fastq_files)})!"
|
|
763
|
+
)
|
|
764
|
+
return self
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
class GrzSubmissionMetadata(StrictBaseModel):
|
|
768
|
+
"""
|
|
769
|
+
General metadata schema for submissions to the GRZ
|
|
770
|
+
"""
|
|
771
|
+
|
|
772
|
+
submission: Submission
|
|
773
|
+
|
|
774
|
+
donors: list[Donor]
|
|
775
|
+
"""
|
|
776
|
+
List of donors including the index patient.
|
|
777
|
+
"""
|
|
778
|
+
|
|
779
|
+
@model_validator(mode="after")
|
|
780
|
+
def validate_donor_count(self):
|
|
781
|
+
"""
|
|
782
|
+
Check whether the submission has the required number of donors based on the study type.
|
|
783
|
+
"""
|
|
784
|
+
study_type = self.submission.genomic_study_type
|
|
785
|
+
|
|
786
|
+
match study_type:
|
|
787
|
+
case GenomicStudyType.single:
|
|
788
|
+
# Check if the submission has at least one donor
|
|
789
|
+
if not self.donors:
|
|
790
|
+
raise ValueError(
|
|
791
|
+
"At least one donor is required for a single study."
|
|
792
|
+
)
|
|
793
|
+
case GenomicStudyType.duo:
|
|
794
|
+
# Check if the submission has at least two donors
|
|
795
|
+
if len(self.donors) < 2:
|
|
796
|
+
raise ValueError(
|
|
797
|
+
"At least two donors are required for a duo study."
|
|
798
|
+
)
|
|
799
|
+
case GenomicStudyType.trio:
|
|
800
|
+
# Check if the submission has at least three donors
|
|
801
|
+
if len(self.donors) < 3:
|
|
802
|
+
raise ValueError(
|
|
803
|
+
"At least three donors are required for a trio study."
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
return self
|
|
807
|
+
|
|
808
|
+
@model_validator(mode="after")
|
|
809
|
+
def validate_minimum_mean_coverage(self):
|
|
810
|
+
"""
|
|
811
|
+
Check if the submission meets the minimum mean coverage requirements.
|
|
812
|
+
"""
|
|
813
|
+
thresholds_somatic = {
|
|
814
|
+
(LibraryType.panel, SequenceSubtype.somatic): 500,
|
|
815
|
+
(LibraryType.wes, SequenceSubtype.somatic): 250,
|
|
816
|
+
(LibraryType.wes, SequenceSubtype.germline): 100,
|
|
817
|
+
(LibraryType.wgs, SequenceSubtype.somatic): 80,
|
|
818
|
+
(LibraryType.wgs, SequenceSubtype.germline): 40,
|
|
819
|
+
}
|
|
820
|
+
thresholds_rare_disease = {(LibraryType.wgs, SequenceSubtype.germline): 30}
|
|
821
|
+
|
|
822
|
+
study_subtype = self.submission.genomic_study_subtype
|
|
823
|
+
thresholds = (
|
|
824
|
+
thresholds_rare_disease
|
|
825
|
+
if study_subtype == GenomicStudySubtype.germline_only
|
|
826
|
+
else thresholds_somatic
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
for donor in self.donors:
|
|
830
|
+
for lab_datum in donor.lab_data:
|
|
831
|
+
threshold = thresholds.get(
|
|
832
|
+
(lab_datum.library_type, lab_datum.sequence_subtype)
|
|
833
|
+
)
|
|
834
|
+
if not threshold:
|
|
835
|
+
raise ValueError(
|
|
836
|
+
f"Threshold for {lab_datum.library_type} {lab_datum.sequence_subtype} not found."
|
|
837
|
+
)
|
|
838
|
+
sequence_data = lab_datum.sequence_data
|
|
839
|
+
if sequence_data.mean_depth_of_coverage < threshold:
|
|
840
|
+
raise ValueError(
|
|
841
|
+
f"Mean depth of coverage for donor '{donor.case_id}', lab datum '{lab_datum.lab_data_name}' "
|
|
842
|
+
f"below threshold ({threshold}): {sequence_data.mean_depth_of_coverage}"
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
return self
|