Skip to content

Commit 8cfe3f1

Browse files
author
Virag Sharma
committed
fix: remove stale fallback methods and update validation tests after grz-check refactor
1 parent ebf4501 commit 8cfe3f1

2 files changed

Lines changed: 45 additions & 330 deletions

File tree

packages/grz-common/src/grz_common/workers/submission.py

Lines changed: 0 additions & 317 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)