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.
grz_cli/parser.py ADDED
@@ -0,0 +1,771 @@
1
+ """Classes for parsing and validating submission metadata and files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from collections.abc import Generator
8
+ from itertools import groupby
9
+ from os import PathLike
10
+ from pathlib import Path
11
+
12
+ from .download import S3BotoDownloadWorker
13
+ from .fastq_validation import validate_paired_end_reads, validate_single_end_reads
14
+ from .file_operations import Crypt4GH, calculate_sha256
15
+ from .models.config import ConfigModel
16
+ from .models.v1_0_0.metadata import File as SubmissionFileMetadata
17
+ from .models.v1_0_0.metadata import (
18
+ FileType,
19
+ GrzSubmissionMetadata,
20
+ ReadOrder,
21
+ SequenceData,
22
+ SequenceType,
23
+ SequencingLayout,
24
+ )
25
+ from .upload import S3BotoUploadWorker
26
+
27
+ log = logging.getLogger(__name__)
28
+
29
+
30
+ class SubmissionMetadata:
31
+ """Class for reading and validating submission metadata"""
32
+
33
+ __log = log.getChild("SubmissionMetadata")
34
+
35
+ def __init__(self, metadata_file):
36
+ """
37
+ Load, parse and validate the metadata file.
38
+
39
+ :param metadata_file: path to the metadata.json file
40
+ :raises json.JSONDecodeError: if failed to read the metadata.json file
41
+ :raises jsonschema.exceptions.ValidationError: if metadata does not match expected schema
42
+ """
43
+ self.file_path = metadata_file
44
+ self.content = self._read_metadata(self.file_path)
45
+ self._checksum = calculate_sha256(self.file_path, progress=False)
46
+
47
+ self._files = None
48
+
49
+ @classmethod
50
+ def _read_metadata(cls, file_path: Path) -> GrzSubmissionMetadata:
51
+ """
52
+ Load and parse the metadata file in JSON format.
53
+
54
+ :param file_path: Path to the metadata JSON file
55
+ :return: Parsed metadata as a dictionary
56
+ :raises json.JSONDecodeError: if failed to read the metadata.json file
57
+ """
58
+ try:
59
+ with open(file_path, encoding="utf-8") as jsonfile:
60
+ metadata = json.load(jsonfile)
61
+ metadata_model = GrzSubmissionMetadata(**metadata)
62
+ return metadata_model
63
+ except json.JSONDecodeError as e:
64
+ cls.__log.error("Invalid JSON format in metadata file: %s", file_path)
65
+ raise e
66
+
67
+ @property
68
+ def index_case_id(self) -> str:
69
+ """
70
+ The index case ID of this submission
71
+ """
72
+ return self.content.submission.tan_g
73
+
74
+ @property
75
+ def files(self) -> dict[Path, SubmissionFileMetadata]:
76
+ """
77
+ The files linked in the metadata.
78
+
79
+ :return: Dictionary of `file_path` -> `SubmissionFileMetadata` pairs.
80
+ Each `file_path` refers to the relative file path from the metadata.
81
+ """
82
+ if self._files is not None:
83
+ return self._files
84
+
85
+ submission_files = {}
86
+ for donor in self.content.donors:
87
+ for lab_data in donor.lab_data:
88
+ for file_data in lab_data.sequence_data.files:
89
+ file_path = Path(file_data.file_path)
90
+ if file_path.is_symlink():
91
+ raise ValueError(
92
+ f"Provided path is a symlink which is not accepted: {file_path}"
93
+ )
94
+ else:
95
+ submission_files[file_path] = file_data
96
+
97
+ self._files = submission_files
98
+ return self._files
99
+
100
+ def validate(self) -> Generator[str]:
101
+ """
102
+ Validates this submission's metadata (content).
103
+
104
+ :return: Generator of errors
105
+ """
106
+ submission_files: dict[str | PathLike, SubmissionFileMetadata] = {}
107
+ for donor in self.content.donors:
108
+ for lab_data in donor.lab_data:
109
+ for file_data in lab_data.sequence_data.files:
110
+ # check if file is already registered
111
+ file_path = Path(file_data.file_path)
112
+ if other_metadata := submission_files.get(file_path):
113
+ # check if metadata matches
114
+ if file_data != other_metadata:
115
+ yield f"{file_data.file_path}: Different metadata for the same path observed!"
116
+
117
+ # check if FASTQ data was already linked in another submission
118
+ if file_data.file_type == "fastq":
119
+ yield f"{file_data.file_path}: FASTQ file already linked in another submission!"
120
+ if file_data.file_type == "bam":
121
+ yield f"{file_data.file_path}: BAM file already linked in another submission!"
122
+ else:
123
+ submission_files[file_path] = file_data
124
+
125
+ @property
126
+ def checksum(self) -> str:
127
+ """
128
+ Checksum of the metadata file
129
+ """
130
+ return self._checksum
131
+
132
+
133
+ class Submission:
134
+ """Class for handling submission data"""
135
+
136
+ __log = log.getChild("Submission")
137
+
138
+ def __init__(self, metadata_dir: str | PathLike, files_dir: str | PathLike):
139
+ """
140
+ Initialize the submission object.
141
+
142
+ :param metadata_dir: Path to the metadata directory
143
+ :param files_dir: Path to the files directory
144
+ """
145
+ self.metadata_dir = Path(metadata_dir)
146
+ self.files_dir = Path(files_dir)
147
+
148
+ self.metadata = SubmissionMetadata(self.metadata_dir / "metadata.json")
149
+
150
+ @property
151
+ def files(self) -> dict[Path, SubmissionFileMetadata]:
152
+ """
153
+ The files liked in the metadata.
154
+
155
+ :return: Dictionary of `local_file_path` -> `SubmissionFileMetadata` pairs.
156
+ """
157
+ retval = {}
158
+ for file_path, file_metadata in self.metadata.files.items():
159
+ local_file_path = self.files_dir / file_path
160
+
161
+ retval[local_file_path] = file_metadata
162
+
163
+ return retval
164
+
165
+ def validate_checksums(self, progress_log_file: str | PathLike) -> Generator[str]:
166
+ """
167
+ Validates the checksum of the files against the metadata and prints the errors.
168
+
169
+ :return: Generator of errors
170
+ """
171
+ from .progress_logging import FileProgressLogger
172
+
173
+ progress_logger = FileProgressLogger(log_file_path=progress_log_file)
174
+ # cleanup log file and keep only files listed here
175
+ progress_logger.cleanup(
176
+ keep=[
177
+ (file_path, file_metadata)
178
+ for file_path, file_metadata in self.files.items()
179
+ ]
180
+ )
181
+ # fields:
182
+ # - "errors": List[str]
183
+ # - "validation_passed": bool
184
+
185
+ for local_file_path, file_metadata in self.files.items():
186
+ logged_state = progress_logger.get_state(local_file_path, file_metadata)
187
+
188
+ # determine if we can skip the verification
189
+ if logged_state is None:
190
+ self.__log.debug("State for %s not calculated yet", local_file_path)
191
+ elif not logged_state.get("validation_passed", False):
192
+ errors = logged_state.get("errors", [])
193
+ yield from errors
194
+
195
+ # skip re-verification
196
+ continue
197
+ else:
198
+ self.__log.debug(
199
+ "Validation for %s already passed, skipping...",
200
+ str(local_file_path),
201
+ )
202
+
203
+ # skip re-verification
204
+ continue
205
+
206
+ self.__log.debug("Validating '%s'...", str(local_file_path))
207
+ # validate the file
208
+ errors = list(file_metadata.validate_data(local_file_path))
209
+ validation_passed = len(errors) == 0
210
+
211
+ # log state
212
+ progress_logger.set_state(
213
+ local_file_path,
214
+ file_metadata,
215
+ state={
216
+ "errors": errors,
217
+ "validation_passed": validation_passed,
218
+ },
219
+ )
220
+
221
+ yield from errors
222
+
223
+ def validate_sequencing_data(
224
+ self, progress_log_file: str | PathLike
225
+ ) -> Generator[str]:
226
+ """
227
+ Quick-validates sequencing data linked in this submission.
228
+
229
+ :return: Generator of errors
230
+ """
231
+
232
+ def find_fastq_files(sequence_data: SequenceData):
233
+ return [f for f in sequence_data.files if f.file_type == FileType.fastq]
234
+
235
+ for donor in self.metadata.content.donors:
236
+ for lab_data in donor.lab_data:
237
+ sequence_type = lab_data.sequence_type
238
+ sequencing_layout = lab_data.sequencing_layout
239
+ sequence_data = lab_data.sequence_data
240
+ if sequence_type == SequenceType.dna:
241
+ # find all FASTQ files
242
+ fastq_files = find_fastq_files(sequence_data)
243
+
244
+ if sequencing_layout == SequencingLayout.single_end:
245
+ yield from validate_single_end_reads(
246
+ self.files_dir / fastq_files[0].file_path
247
+ )
248
+ else:
249
+ # group reads by flowcell id and lane id
250
+ key = lambda f: (f.flowcell_id, f.lane_id)
251
+ fastq_files.sort(key=key)
252
+ for _key, group in groupby(fastq_files, key):
253
+ files = list(group)
254
+
255
+ # separate R1 and R2 files
256
+ fastq_r1_files = [
257
+ f for f in files if f.read_order == ReadOrder.r1
258
+ ]
259
+ fastq_r2_files = [
260
+ f for f in files if f.read_order == ReadOrder.r2
261
+ ]
262
+
263
+ for fastq_r1, fastq_r2 in zip(
264
+ fastq_r1_files, fastq_r2_files, strict=True
265
+ ):
266
+ yield from validate_paired_end_reads(
267
+ # fastq R1
268
+ self.files_dir / fastq_r1.file_path,
269
+ # fastq R2
270
+ self.files_dir / fastq_r2.file_path,
271
+ )
272
+
273
+ elif sequence_type == SequenceType.rna:
274
+ # TODO: What to check here?
275
+ pass
276
+
277
+ def encrypt(
278
+ self,
279
+ encrypted_files_dir: str | PathLike,
280
+ progress_log_file: str | PathLike,
281
+ recipient_public_key_path: str | PathLike,
282
+ submitter_private_key_path: str | PathLike | None = None,
283
+ ) -> EncryptedSubmission:
284
+ """
285
+ Encrypt this submission with a public key using Crypt4Gh
286
+
287
+ :param encrypted_files_dir: Output directory of the encrypted files
288
+ :param progress_log_file: Path to a log file to store the progress of the encryption process
289
+ :param recipient_public_key_path: Path to the public key file which will be used for encryption
290
+ :param submitter_private_key_path: Path to the private key file which will be used to sign the encryption
291
+ :return: EncryptedSubmission instance
292
+ """
293
+ encrypted_files_dir = Path(encrypted_files_dir)
294
+
295
+ if not Path(recipient_public_key_path).expanduser().is_file():
296
+ msg = f"Public key file does not exist: {recipient_public_key_path}"
297
+ self.__log.error(msg)
298
+ raise FileNotFoundError(msg)
299
+ if not submitter_private_key_path:
300
+ self.__log.warning("No submitter private key provided, skipping signing.")
301
+ elif not Path(submitter_private_key_path).expanduser().is_file():
302
+ msg = f"Private key file does not exist: {submitter_private_key_path}"
303
+ self.__log.error(msg)
304
+ raise FileNotFoundError(msg)
305
+
306
+ if not encrypted_files_dir.is_dir():
307
+ self.__log.debug(
308
+ "Creating encrypted submission files directory: %s...",
309
+ encrypted_files_dir,
310
+ )
311
+ encrypted_files_dir.mkdir(mode=0o770, parents=False, exist_ok=False)
312
+
313
+ from .progress_logging import FileProgressLogger
314
+
315
+ progress_logger = FileProgressLogger(log_file_path=progress_log_file)
316
+
317
+ try:
318
+ public_keys = Crypt4GH.prepare_c4gh_keys(recipient_public_key_path)
319
+ except Exception as e:
320
+ self.__log.error(f"Error preparing public keys: {e}")
321
+ raise e
322
+
323
+ for file_path, file_metadata in self.files.items():
324
+ # encryption_successful = True
325
+ logged_state = progress_logger.get_state(file_path, file_metadata)
326
+ self.__log.debug("state for %s: %s", file_path, logged_state)
327
+
328
+ encrypted_file_path = (
329
+ encrypted_files_dir
330
+ / EncryptedSubmission.get_encrypted_file_path(file_metadata.file_path)
331
+ )
332
+ encrypted_file_path.parent.mkdir(mode=0o770, parents=True, exist_ok=True)
333
+
334
+ if (
335
+ (logged_state is None)
336
+ or not logged_state.get("encryption_successful", False)
337
+ or not encrypted_file_path.is_file()
338
+ ):
339
+ self.__log.info(
340
+ "Encrypting file: '%s' -> '%s'",
341
+ str(file_path),
342
+ str(encrypted_file_path),
343
+ )
344
+
345
+ try:
346
+ Crypt4GH.encrypt_file(file_path, encrypted_file_path, public_keys)
347
+
348
+ self.__log.info(f"Encryption complete for {str(file_path)}. ")
349
+ progress_logger.set_state(
350
+ file_path, file_metadata, state={"encryption_successful": True}
351
+ )
352
+ except Exception as e:
353
+ self.__log.error("Encryption failed for '%s'", str(file_path))
354
+
355
+ progress_logger.set_state(
356
+ file_path,
357
+ file_metadata,
358
+ state={"encryption_successful": False, "error": str(e)},
359
+ )
360
+
361
+ raise e
362
+ else:
363
+ self.__log.info(
364
+ "File '%s' already encrypted in '%s'",
365
+ str(file_path),
366
+ str(encrypted_file_path),
367
+ )
368
+
369
+ self.__log.info("File encryption completed.")
370
+
371
+ return EncryptedSubmission(
372
+ metadata_dir=self.metadata_dir,
373
+ encrypted_files_dir=encrypted_files_dir,
374
+ )
375
+
376
+
377
+ class EncryptedSubmission:
378
+ """The encrypted counterpart to `Submission`. Handles encrypted submission data."""
379
+
380
+ __log = log.getChild("EncryptedSubmission")
381
+
382
+ def __init__(
383
+ self, metadata_dir: str | PathLike, encrypted_files_dir: str | PathLike
384
+ ):
385
+ """
386
+ Initialize the encrypted submission object.
387
+
388
+ :param metadata_dir: Path to the metadata directory
389
+ :param encrypted_files_dir: Path to the encrypted files directory
390
+ """
391
+ self.metadata_dir = Path(metadata_dir)
392
+ self.encrypted_files_dir = Path(encrypted_files_dir)
393
+
394
+ self.metadata = SubmissionMetadata(self.metadata_dir / "metadata.json")
395
+
396
+ @property
397
+ def encrypted_files(self):
398
+ """
399
+ The encrypted files liked in the metadata.
400
+
401
+ :return: Dictionary of `local_file_path` -> `SubmissionFileMetadata` pairs.
402
+ """
403
+ retval = {}
404
+ for file_path, file_metadata in self.metadata.files.items():
405
+ encrypted_file_path = self.get_encrypted_file_path(
406
+ self.encrypted_files_dir / file_path
407
+ )
408
+
409
+ retval[encrypted_file_path] = file_metadata
410
+
411
+ return retval
412
+
413
+ def get_metadata_file_path_and_object_id(self) -> tuple[Path, str]:
414
+ """
415
+ :return: tuple with the `local_file_path` and s3_object_id of the metadata file
416
+ """
417
+ return Path(self.metadata.file_path), str(
418
+ Path(self.metadata.index_case_id)
419
+ / "metadata"
420
+ / self.metadata.file_path.name
421
+ )
422
+
423
+ def get_encrypted_files_and_object_id(self) -> dict[Path, str]:
424
+ """
425
+ :return Dictionary of `local_file_path` -> s3_object_id
426
+ """
427
+ retval = {}
428
+ for local_file_path, file_metadata in self.encrypted_files.items():
429
+ retval[local_file_path] = str(
430
+ Path(self.metadata.index_case_id)
431
+ / "files"
432
+ / self.get_encrypted_file_path(file_metadata.file_path)
433
+ )
434
+ return retval
435
+
436
+ @staticmethod
437
+ def get_encrypted_file_path(file_path: str | PathLike) -> Path:
438
+ """
439
+ Return the path to the encrypted file based on the original file path,
440
+ with additional extension'.c4gh'.
441
+ """
442
+ p = Path(file_path)
443
+ return p.with_suffix(p.suffix + ".c4gh")
444
+
445
+ @staticmethod
446
+ def get_encryption_header_path(file_path: str | PathLike) -> Path:
447
+ """
448
+ Return the path to the encryption header file based on the original file path,
449
+ with additional extension'.c4gh_header'.
450
+ """
451
+ p = Path(file_path)
452
+ return p.with_suffix(p.suffix + ".c4gh_header")
453
+
454
+ def decrypt(
455
+ self,
456
+ files_dir: str | PathLike,
457
+ progress_log_file: str | PathLike,
458
+ recipient_private_key_path: str | PathLike,
459
+ ) -> Submission:
460
+ """
461
+ Decrypt this encrypted submission with a private key using Crypt4Gh
462
+
463
+ :param files_dir: Output directory of the decrypted files
464
+ :param progress_log_file: Path to a log file to store the progress of the decryption process
465
+ :param recipient_private_key_path: Path to the private key file which will be used for decryption
466
+ :return: Submission instance
467
+ """
468
+ files_dir = Path(files_dir)
469
+
470
+ if not files_dir.is_dir():
471
+ self.__log.debug(
472
+ "Creating decrypted submission files directory: %s...",
473
+ files_dir,
474
+ )
475
+ files_dir.mkdir(mode=0o770, parents=False, exist_ok=False)
476
+
477
+ from .progress_logging import FileProgressLogger
478
+
479
+ progress_logger = FileProgressLogger(log_file_path=progress_log_file)
480
+
481
+ try:
482
+ private_key = Crypt4GH.retrieve_private_key(recipient_private_key_path)
483
+ except Exception as e:
484
+ self.__log.error(f"Error preparing private key: {e}")
485
+ raise e
486
+
487
+ for encrypted_file_path, file_metadata in self.encrypted_files.items():
488
+ logged_state = progress_logger.get_state(encrypted_file_path, file_metadata)
489
+ self.__log.debug("state for %s: %s", encrypted_file_path, logged_state)
490
+
491
+ decrypted_file_path = files_dir / file_metadata.file_path
492
+ if not decrypted_file_path.parent.is_dir():
493
+ decrypted_file_path.parent.mkdir(
494
+ mode=0o770, parents=True, exist_ok=False
495
+ )
496
+
497
+ if (
498
+ (logged_state is None)
499
+ or not logged_state.get("decryption_successful", False)
500
+ or not decrypted_file_path.is_file()
501
+ ):
502
+ self.__log.info(
503
+ "Decrypting file: '%s' -> '%s'",
504
+ str(encrypted_file_path),
505
+ str(decrypted_file_path),
506
+ )
507
+
508
+ try:
509
+ Crypt4GH.decrypt_file(
510
+ encrypted_file_path, decrypted_file_path, private_key
511
+ )
512
+
513
+ self.__log.info(
514
+ f"Decryption complete for {str(encrypted_file_path)}. "
515
+ )
516
+ progress_logger.set_state(
517
+ encrypted_file_path,
518
+ file_metadata,
519
+ state={"decryption_successful": True},
520
+ )
521
+ except Exception as e:
522
+ self.__log.error(
523
+ "Decryption failed for '%s'", str(encrypted_file_path)
524
+ )
525
+
526
+ progress_logger.set_state(
527
+ encrypted_file_path,
528
+ file_metadata,
529
+ state={"decryption_successful": False, "error": str(e)},
530
+ )
531
+
532
+ raise e
533
+ else:
534
+ self.__log.info(
535
+ "File '%s' already decrypted in '%s'",
536
+ str(encrypted_file_path),
537
+ str(decrypted_file_path),
538
+ )
539
+
540
+ self.__log.info("File decryption completed.")
541
+
542
+ return Submission(
543
+ metadata_dir=self.metadata_dir,
544
+ files_dir=files_dir,
545
+ )
546
+
547
+
548
+ class SubmissionValidationError(Exception):
549
+ """Exception raised when validation of a submission fails"""
550
+
551
+ pass
552
+
553
+
554
+ class Worker:
555
+ """Worker class for handling submission processing"""
556
+
557
+ __log = log.getChild("Worker")
558
+
559
+ def __init__(
560
+ self,
561
+ metadata_dir: str | PathLike,
562
+ files_dir: str | PathLike,
563
+ log_dir: str | PathLike,
564
+ encrypted_files_dir: str | PathLike,
565
+ threads: int = 1,
566
+ ):
567
+ """
568
+ Initialize the worker object.
569
+
570
+ :param metadata_dir: Path to the metadata directory
571
+ :param files_dir: Path to the files directory
572
+ :param log_dir: Path to the log directory
573
+ :param encrypted_files_dir: Path to the encrypted files directory
574
+ :param threads: Number of threads to use
575
+ """
576
+ self._threads = threads
577
+ self.__log.debug("Threads: %s", self._threads)
578
+
579
+ # metadata dir
580
+ self.metadata_dir = Path(metadata_dir)
581
+ self.__log.debug("Metadata directory: %s", self.metadata_dir)
582
+
583
+ # files dir
584
+ self.files_dir = Path(files_dir)
585
+ self.__log.debug("Files directory: %s", self.files_dir)
586
+
587
+ # encrypted files dir
588
+ self.encrypted_files_dir = (
589
+ Path(encrypted_files_dir) if encrypted_files_dir is not None else Path()
590
+ )
591
+
592
+ self.__log.info("Encrypted files directory: %s", self.encrypted_files_dir)
593
+
594
+ # log dir
595
+ self.log_dir = Path(log_dir)
596
+ self.__log.info("Log directory: %s", self.log_dir)
597
+
598
+ # create log dir if non-existent
599
+ if not self.log_dir.is_dir():
600
+ self.__log.debug("Creating log directory...")
601
+ self.log_dir.mkdir(mode=0o770, parents=False, exist_ok=False)
602
+
603
+ self.progress_file_checksum_validation = (
604
+ self.log_dir / "progress_checksum_validation.cjson"
605
+ )
606
+ self.progress_file_sequencing_data_validation = (
607
+ self.log_dir / "progress_sequencing_data_validation.cjson"
608
+ )
609
+ self.progress_file_encrypt = self.log_dir / "progress_encrypt.cjson"
610
+ self.progress_file_decrypt = self.log_dir / "progress_decrypt.cjson"
611
+ self.progress_file_upload = self.log_dir / "progress_upload.cjson"
612
+ self.progress_file_download = self.log_dir / "progress_download.cjson"
613
+
614
+ def parse_submission(self) -> Submission:
615
+ """
616
+ Reads the submission metadata and returns a Submission instance
617
+ """
618
+ submission = Submission(
619
+ metadata_dir=self.metadata_dir,
620
+ files_dir=self.files_dir,
621
+ )
622
+ return submission
623
+
624
+ def parse_encrypted_submission(self) -> EncryptedSubmission:
625
+ """
626
+ Reads the submission metadata and returns an EncryptedSubmission instance
627
+ """
628
+ encrypted_submission = EncryptedSubmission(
629
+ metadata_dir=self.metadata_dir,
630
+ encrypted_files_dir=str(self.encrypted_files_dir),
631
+ )
632
+ return encrypted_submission
633
+
634
+ def validate(self, force=False):
635
+ """
636
+ Validate this submission
637
+
638
+ :param force: Force validation of already validated files
639
+ :raises SubmissionValidationError: if the validation fails
640
+ """
641
+ submission = self.parse_submission()
642
+
643
+ self.__log.info("Starting metadata validation...")
644
+ if errors := list(submission.metadata.validate()):
645
+ error_msg = "\n".join(["Metadata validation failed! Errors:", *errors])
646
+ self.__log.error(error_msg)
647
+
648
+ raise SubmissionValidationError(error_msg)
649
+ else:
650
+ self.__log.info("Metadata validation successful!")
651
+
652
+ if force:
653
+ # delete the log file
654
+ self.progress_file_checksum_validation.unlink()
655
+
656
+ self.__log.info("Starting checksum validation...")
657
+ if errors := list(
658
+ submission.validate_checksums(
659
+ progress_log_file=self.progress_file_checksum_validation
660
+ )
661
+ ):
662
+ error_msg = "\n".join(["Checksum validation failed! Errors:", *errors])
663
+ self.__log.error(error_msg)
664
+
665
+ raise SubmissionValidationError(error_msg)
666
+ else:
667
+ self.__log.info("Checksum validation successful!")
668
+ self.__log.info("Starting checksum validation...")
669
+
670
+ if errors := list(
671
+ submission.validate_sequencing_data(
672
+ progress_log_file=self.progress_file_sequencing_data_validation
673
+ )
674
+ ):
675
+ error_msg = "\n".join(["Checksum validation failed! Errors:", *errors])
676
+ self.__log.error(error_msg)
677
+
678
+ raise SubmissionValidationError(error_msg)
679
+ else:
680
+ self.__log.info("Checksum validation successful!")
681
+
682
+ # TODO: validate FASTQ
683
+
684
+ def encrypt(
685
+ self,
686
+ recipient_public_key_path: str | PathLike,
687
+ submitter_private_key_path: str | PathLike | None = None,
688
+ force=False,
689
+ ) -> EncryptedSubmission:
690
+ """
691
+ Encrypt this submission with a public key using Crypt4Gh.
692
+ :param recipient_public_key_path: Path to the public key file of the recipient.
693
+ :param submitter_private_key_path: Path to the private key file of the submitter.
694
+ :param force: Force encryption of already encrypted files
695
+ :return: EncryptedSubmission instance
696
+ """
697
+ submission = self.parse_submission()
698
+
699
+ if force:
700
+ # delete the log file
701
+ self.progress_file_encrypt.unlink()
702
+
703
+ encrypted_submission = submission.encrypt(
704
+ encrypted_files_dir=str(self.encrypted_files_dir),
705
+ progress_log_file=self.progress_file_encrypt,
706
+ recipient_public_key_path=recipient_public_key_path,
707
+ submitter_private_key_path=submitter_private_key_path,
708
+ )
709
+
710
+ return encrypted_submission
711
+
712
+ def decrypt(
713
+ self, recipient_private_key_path: str | PathLike, force=False
714
+ ) -> Submission:
715
+ """
716
+ Encrypt this submission with a public key using Crypt4Gh.
717
+ :param recipient_public_key_path: Path to the public key file of the recipient.
718
+ :param submitter_private_key_path: Path to the private key file of the submitter.
719
+ :param force: Force decryption of already decrypted files
720
+ :return: EncryptedSubmission instance
721
+ """
722
+ encrypted_submission = self.parse_encrypted_submission()
723
+
724
+ if force:
725
+ # delete the log file
726
+ self.progress_file_decrypt.unlink()
727
+
728
+ submission = encrypted_submission.decrypt(
729
+ files_dir=self.files_dir,
730
+ progress_log_file=self.progress_file_decrypt,
731
+ recipient_private_key_path=recipient_private_key_path,
732
+ )
733
+
734
+ return submission
735
+
736
+ def upload(self, config: ConfigModel):
737
+ """
738
+ Upload an encrypted submission
739
+
740
+ """
741
+ upload_worker = S3BotoUploadWorker(
742
+ config, status_file_path=self.progress_file_upload
743
+ )
744
+
745
+ encrypted_submission = self.parse_encrypted_submission()
746
+
747
+ upload_worker.upload(encrypted_submission)
748
+
749
+ def download(self, config: ConfigModel, submission_id: str):
750
+ """
751
+ Download an encrypted submission
752
+ """
753
+ download_worker = S3BotoDownloadWorker(
754
+ config, status_file_path=self.progress_file_upload
755
+ )
756
+
757
+ self.__log.info("Preparing output directories...")
758
+ download_worker.prepare_download(
759
+ self.metadata_dir, self.encrypted_files_dir, self.log_dir
760
+ )
761
+
762
+ self.__log.info("Downloading metadata...")
763
+ download_worker.download_metadata(
764
+ submission_id, self.metadata_dir, metadata_file_name="metadata.json"
765
+ )
766
+
767
+ self.__log.info("Downloading encrypted files...")
768
+ download_worker.download(
769
+ submission_id,
770
+ EncryptedSubmission(self.metadata_dir, self.encrypted_files_dir),
771
+ )