@@ -470,323 +470,6 @@ def _execute_task(task_type, paths, metas, kwargs, pbar):
470470
471471 yield from self ._aggregate_validation_errors (checksum_progress_logger , seq_data_progress_logger )
472472
473- def _run_grz_check_command (
474- self ,
475- grz_check_args : list [str ],
476- threads : int | None ,
477- log_dir : Path ,
478- checksum_logger : FileProgressLogger ,
479- seq_logger : FileProgressLogger ,
480- ) -> Generator [str , None , None ]:
481- """Helper to encapsulate the subprocess execution and report processing."""
482- temp_report_path = log_dir / "grz-check.report.jsonl"
483- temp_report_path .unlink (missing_ok = True )
484-
485- command_args = ["--output" , str (temp_report_path ), * grz_check_args ]
486- if threads :
487- command_args .extend (["--threads" , str (threads )])
488-
489- try :
490- run_grz_check (command_args )
491- except UserInterruptException :
492- self .__log .warning ("Validation cancelled by user. Processing partial results..." )
493- raise
494- except subprocess .CalledProcessError as e :
495- self .__log .error (f"`grz-check` failed with exit code { e .returncode } " )
496- yield "`grz-check` execution failed. See logs for details."
497- finally :
498- if temp_report_path .is_file ():
499- with temp_report_path .open ("r" ) as f :
500- self ._process_grz_check_report (f , checksum_logger , seq_logger )
501- temp_report_path .unlink ()
502-
503- def _aggregate_validation_errors (
504- self , checksum_progress_logger : FileProgressLogger , seq_data_progress_logger : FileProgressLogger
505- ) -> Generator [str , None , None ]:
506- """Aggregates all errors from both progress loggers into a flat generator."""
507- all_errors = set ()
508- for local_file_path , file_metadata in self .files .items ():
509- checksum_state = checksum_progress_logger .get_state (local_file_path , file_metadata )
510- if checksum_state and not checksum_state .get ("validation_passed" ):
511- for error in checksum_state .get ("errors" , []):
512- all_errors .add (f"{ local_file_path .relative_to (self .files_dir )} : { error } " )
513-
514- if file_metadata .file_type in ("fastq" , "bam" ):
515- seq_data_state = seq_data_progress_logger .get_state (local_file_path , file_metadata )
516- if seq_data_state and not seq_data_state .get ("validation_passed" ):
517- for error in seq_data_state .get ("errors" , []):
518- all_errors .add (f"{ local_file_path .relative_to (self .files_dir )} : { error } " )
519- yield from all_errors
520-
521- def _process_grz_check_report ( # noqa: C901
522- self ,
523- report_file : typing .TextIO ,
524- checksum_progress_logger : FileProgressLogger [ValidationState ],
525- seq_data_progress_logger : FileProgressLogger [ValidationState ],
526- ):
527- """
528- Parses the JSONL report from `grz-check` and updates both progress loggers.
529- """
530- for line in report_file :
531- try :
532- report_entry = json .loads (line )
533- data = report_entry .get ("data" , {})
534- file_path_str = data .get ("path" )
535- if not file_path_str :
536- continue
537-
538- file_path = Path (file_path_str ).resolve ()
539- file_metadata = self .files .get (file_path )
540-
541- if not file_metadata :
542- self .__log .warning (f"Could not find metadata for file in grz-check report: { file_path_str } " )
543- continue
544-
545- status = data .get ("status" )
546- errors = data .get ("errors" , [])
547- warnings = data .get ("warnings" , [])
548- checksum = data .get ("checksum" )
549-
550- if warnings :
551- for w in warnings :
552- self .__log .warning (w )
553-
554- checksum_issues = []
555- if (
556- checksum
557- and (file_metadata .checksum_type or "" ).lower () == "sha256"
558- and file_metadata .file_checksum != checksum
559- ):
560- checksum_issues .append (
561- f"Checksum mismatch! Expected: '{ file_metadata .file_checksum } ', calculated: '{ checksum } '"
562- )
563-
564- if file_path .exists () and file_path .is_file ():
565- if file_metadata .file_size_in_bytes != file_path .stat ().st_size :
566- checksum_issues .append (
567- f"File size mismatch! Expected: '{ file_metadata .file_size_in_bytes } ', observed: '{ file_path .stat ().st_size } '."
568- )
569- else :
570- checksum_issues .append ("File not found for size check." )
571-
572- checksum_passed = not checksum_issues
573- checksum_state = ValidationState (
574- errors = checksum_issues , validation_passed = checksum_passed , submission_id = self .submission_id
575- )
576- checksum_progress_logger .set_state (file_path , file_metadata , checksum_state )
577-
578- if file_metadata .file_type in ("fastq" , "bam" ):
579- integrity_passed = status == "OK"
580- integrity_errors = errors if not integrity_passed else []
581-
582- seq_data_state = ValidationState (
583- errors = integrity_errors , validation_passed = integrity_passed , submission_id = self .submission_id
584- )
585- seq_data_progress_logger .set_state (file_path , file_metadata , seq_data_state )
586-
587- except json .JSONDecodeError :
588- self .__log .warning (f"Could not parse line in grz-check report: { line .strip ()} " )
589- except Exception as e :
590- self .__log .error (f"Error processing grz-check report entry: { line .strip ()} . Error: { e } " )
591-
592- @staticmethod
593- def _validate_file_data_fallback (metadata : SubmissionFileMetadata , local_file_path : Path ) -> Generator [str ]:
594- """
595- Validates whether the provided file matches this metadata.
596- (Fallback method)
597-
598- :param metadata: Metadata model object
599- :param local_file_path: Path to the actual file (resolved if symlinked)
600- :return: Generator of errors
601- """
602- # Resolve file path
603- local_file_path = local_file_path .resolve ()
604-
605- # Check if path exists
606- if not local_file_path .exists ():
607- yield f"{ str (Path ('files' ) / metadata .file_path )} does not exist! Ensure filePath is relative to the files/ directory under the submission root."
608- # Return here as following tests cannot work
609- return
610-
611- # Check if path is a file
612- if not local_file_path .is_file ():
613- yield f"{ str (metadata .file_path )} is not a file!"
614- # Return here as following tests cannot work
615- return
616-
617- # Check if the checksum is correct
618- if metadata .checksum_type == "sha256" :
619- calculated_checksum = calculate_sha256 (local_file_path )
620- if metadata .file_checksum != calculated_checksum :
621- yield (
622- f"{ str (metadata .file_path )} : Checksum mismatch! "
623- f"Expected: '{ metadata .file_checksum } ', calculated: '{ calculated_checksum } '."
624- )
625- else :
626- yield (
627- f"{ str (metadata .file_path )} : Unsupported checksum type: { metadata .checksum_type } . "
628- f"Supported types: { [e .value for e in ChecksumType ]} "
629- )
630-
631- # Check file size
632- if metadata .file_size_in_bytes != local_file_path .stat ().st_size :
633- yield (
634- f"{ str (metadata .file_path )} : File size mismatch! "
635- f"Expected: '{ metadata .file_size_in_bytes } ', observed: '{ local_file_path .stat ().st_size } '."
636- )
637-
638- def _validate_checksums_fallback (self , progress_log_file : str | PathLike ) -> Generator [str ]:
639- """
640- Validates the checksum of the files against the metadata.
641- (Fallback method)
642-
643- :return: Generator of errors
644- """
645- progress_logger = FileProgressLogger [ValidationState ](log_file_path = progress_log_file )
646- # cleanup log file and keep only files listed here
647- progress_logger .cleanup (keep = [(file_path , file_metadata ) for file_path , file_metadata in self .files .items ()])
648- # fields:
649- # - "errors": List[str]
650- # - "validation_passed": bool
651-
652- def validate_file (local_file_path , file_metadata ):
653- self .__log .debug ("Validating '%s'..." , str (local_file_path ))
654-
655- # validate the file
656- errors = list (self ._validate_file_data_fallback (file_metadata , local_file_path ))
657- validation_passed = len (errors ) == 0
658-
659- # return log state
660- return ValidationState (errors = errors , validation_passed = validation_passed , submission_id = self .submission_id )
661-
662- for local_file_path , file_metadata in self .files .items ():
663- logged_state = progress_logger .get_state (local_file_path , file_metadata )
664- if (
665- logged_state is None
666- or not logged_state .get ("validation_passed" )
667- or logged_state .get ("submission_id" ) != self .submission_id
668- ):
669- logged_state = validate_file (local_file_path , file_metadata )
670- progress_logger .set_state (local_file_path , file_metadata , logged_state )
671- if logged_state :
672- yield from logged_state ["errors" ]
673-
674- def _validate_sequencing_data_fallback (self , progress_log_file : str | PathLike ) -> Generator [str ]:
675- """
676- Quick-validates sequencing data linked in this submission.
677- (Fallback method)
678-
679- :return: Generator of errors
680- """
681- # Import here to avoid circular import issues
682- from ..progress import FileProgressLogger # noqa: PLC0415
683-
684- progress_logger = FileProgressLogger [ValidationState ](log_file_path = progress_log_file )
685- # cleanup log file and keep only files listed here
686- progress_logger .cleanup (keep = [(file_path , file_metadata ) for file_path , file_metadata in self .files .items ()])
687-
688- yield from self ._validate_paired_end_fallback (progress_logger )
689- yield from self ._validate_single_end_fallback (progress_logger )
690- yield from self ._validate_bams_fallback (progress_logger )
691-
692- def _validate_bams_fallback (
693- self ,
694- progress_logger : FileProgressLogger [ValidationState ],
695- ) -> Generator [str , None , None ]:
696- """
697- Basic BAM sanity checks.
698- (Fallback method)
699-
700- :param progress_logger: Progress logger
701- """
702-
703- def validate_file (local_file_path , _file_metadata ) -> ValidationState :
704- self .__log .debug ("Validating '%s'..." , str (local_file_path ))
705-
706- # validate the file
707- errors = list (validate_bam (local_file_path ))
708- validation_passed = len (errors ) == 0
709-
710- # return log state
711- return ValidationState (errors = errors , validation_passed = validation_passed , submission_id = self .submission_id )
712-
713- for _donor , _lab_datum , bam_file in self .metadata .iter_bams ():
714- logged_state = progress_logger .get_state (
715- self .files_dir / bam_file .file_path ,
716- bam_file ,
717- default = validate_file , # validate the file if the state was not calculated yet
718- )
719- if logged_state :
720- yield from logged_state ["errors" ]
721-
722- def _validate_single_end_fallback (
723- self ,
724- progress_logger : FileProgressLogger [ValidationState ],
725- ) -> Generator [str , None , None ]:
726- def validate_file (
727- thresholds : Thresholds , local_file_path : Path , file_metadata : SubmissionFileMetadata
728- ) -> ValidationState :
729- self .__log .debug ("Validating '%s'..." , str (local_file_path ))
730-
731- # validate the file
732- mean_read_length_threshold = thresholds .mean_read_length
733- errors = list (
734- validate_single_end_reads (local_file_path , mean_read_length_threshold = mean_read_length_threshold )
735- )
736- validation_passed = len (errors ) == 0
737-
738- # return log state
739- return ValidationState (errors = errors , validation_passed = validation_passed , submission_id = self .submission_id )
740-
741- for _donor , _lab_datum , fastq_files , thresholds in self .metadata .iter_single_end_fastqs ():
742- for fastq_file in fastq_files :
743- logged_state = progress_logger .get_state (
744- self .files_dir / fastq_file .file_path ,
745- fastq_file ,
746- default = partial (validate_file , thresholds ), # validate the file if the state was not calculated yet
747- )
748- if logged_state :
749- yield from logged_state ["errors" ]
750-
751- def _validate_paired_end_fallback (
752- self ,
753- progress_logger : FileProgressLogger [ValidationState ],
754- ) -> Generator [str , None , None ]:
755- for _donor , _lab_datum , fastq_files , thresholds in self .metadata .iter_paired_end_fastqs ():
756- for fastq_r1 , fastq_r2 in fastq_files :
757- local_fastq_r1_path = self .files_dir / fastq_r1 .file_path
758- local_fastq_r2_path = self .files_dir / fastq_r2 .file_path
759-
760- # get saved state
761- logged_state_r1 = progress_logger .get_state (local_fastq_r1_path , fastq_r1 )
762- logged_state_r2 = progress_logger .get_state (local_fastq_r2_path , fastq_r2 )
763-
764- if logged_state_r1 is None or logged_state_r2 is None or logged_state_r1 != logged_state_r2 :
765- # calculate state
766- errors = list (
767- validate_paired_end_reads (
768- local_fastq_r1_path , # fastq R1
769- local_fastq_r2_path , # fastq R2
770- mean_read_length_threshold = thresholds .mean_read_length ,
771- )
772- )
773- validation_passed = len (errors ) == 0
774-
775- state = ValidationState (
776- errors = errors , validation_passed = validation_passed , submission_id = self .submission_id
777- )
778- # update state for both files
779- progress_logger .set_state ( # fastq R1
780- local_fastq_r1_path , fastq_r1 , state
781- )
782- progress_logger .set_state ( # fastq R2
783- local_fastq_r2_path , fastq_r2 , state
784- )
785- yield from state ["errors" ]
786- else :
787- # both fastq states are equal, so simply yield one of them
788- yield from logged_state_r1 ["errors" ]
789-
790473 def encrypt (
791474 self ,
792475 encrypted_files_dir : str | PathLike ,
0 commit comments