fix(deposition): normalise raw-read file extensions to the validated set - #7265
fix(deposition): normalise raw-read file extensions to the validated set#7265corneliusroemer-agent wants to merge 19 commits into
Conversation
… way bioproject and biosample accession being handled is managed
`has_raw_reads_changed` compares a `name -> fileId` mapping, so renaming a raw read file while keeping the same `fileId` counts as a change to the data. The revision then re-uploads byte-identical FASTQs, mints a new ERR/ERX, and puts the old ERR on the list of run accessions that need a suppression email to ENA. For paired reads that is gigabytes of upload and a manual step at ENA, all to record a cosmetic rename. The filename is the wrong thing to key on because it barely reaches ENA at all: `download_fastq_files` names the uploaded file after the `fileId`, so only the extension survives into the manifest, and the stem is discarded. The `fileId` is the faithful identity, and the docstring already excluded the URL for being a short-lived presigned S3 link that changes even when the file does not — this extends the same reasoning to the name. The ids are sorted so that reordering the same two files is not a change either. The reads manifest has no R1/R2 designation, so order carries no meaning to ENA; pairing is inferred from the read names inside the files. https://claude.ai/code/session_01HoLNKkpF4B6EZRynPotieW 🚀 Preview: Add `preview` label to enable --------- Co-authored-by: Cornelius Roemer <cornelius.roemer@pathoplexus.org> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Cornelius Roemer <cornelius.roemer@gmail.com>
download_fastq_files derived the local filename with
`"".join(Path(name).suffixes)` and decided whether to compress with
`endswith((".gz", ".bz2"))`. Both were more general than they needed to
be, and the second was case-sensitive while the upstream check is not.
raw-reads-processing already restricts raw reads to
{.fastq, .fq, .fastq.gz, .fq.gz}, matched case-insensitively, and
preprocessing blocks anything else at submission time. So `.bz2` was
unreachable and the suffix-chain handling was dead generality.
The case mismatch was a live bug: `READS.FASTQ.GZ` is accepted upstream
but `.GZ` != `.gz`, so it was gzipped a second time. Double-gzipped input
passes webin-cli's manifest validation (only the outer layer is
decompressed) and fails later during submit, after the upload has begun.
Replace both with a lookup against the accepted set, returning the
extension lower-cased; compress iff it is not already `.gz`.
Also make the integration mock name files as production does
(<fileId><canonical ext>) rather than basename(name), so these paths are
exercised, and add unit tests for download_fastq_files, which was mocked
everywhere and had no direct coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wgBKxVSaukBXBazE6uGTQ
|
Claude finished @corneliusroemer's task in 2m 24s —— View job ReviewThe core change is right, and it fixes more than the PR description claims. The old Checks I made:
Three inline comments, all minor: hoisting the extension check above the download loop, one test that doesn't assert what its name claims, and an unspecced Two things not worth an inline comment:
I could not run the test suite —
|
There was a problem hiding this comment.
🟡 Changes recommended
The new unit-test mock context manager can inadvertently suppress exceptions, and one updated comment is misleading relative to the enforced extension contract.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes ENA raw-read submission edge cases by normalizing FASTQ filename extensions to the same validated, case-insensitive set used upstream, preventing double-gzip for uppercase .GZ and ensuring webin-cli accepts the resulting filenames.
Changes:
- Introduce
canonical_fastq_extension()and a localACCEPTED_FASTQ_EXTENSIONScontract to lower-case and validate raw-read FASTQ suffixes before download/compression. - Update
download_fastq_files()to decide gzip behavior based on the canonicalized extension (avoiding double compression for e.g.READS.FASTQ.GZ). - Improve unit/integration tests so mocks mirror production naming and so download/compression behavior is actually exercised.
File summaries
| File | Description |
|---|---|
| ena-submission/src/ena_deposition/call_loculus.py | Adds canonical FASTQ extension validation/normalization and updates gzip decision logic. |
| ena-submission/test/test_ena_submission.py | Adds unit tests covering download_fastq_files() naming and compression behaviors. |
| ena-submission/test/test_ena_submission_integration.py | Aligns the integration mock’s output filenames with production behavior using canonicalized extensions. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Only check FASTQ. BAM is BGZF, i.e. a valid gzip stream, so a .bam would have been rejected as "gzip-compressed but not named .gz". Unreachable while ACCEPTED_FORMATS is FASTQ-only, but it would have bitten whoever enables BAM. - Catch EOFError and zlib.error alongside OSError. gzip raises EOFError on a truncated file and zlib.error on corrupt deflate data; neither is an OSError, so both escaped as an unhandled 500 instead of the submitter-facing annotation. Both new tests fail without the wider except. - Drop the error text from that message: some OSErrors carry the local temp path, which should not reach submitters. - Note on the sync pointer that the mirror arrives with #7265; it is not on main yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013wgBKxVSaukBXBazE6uGTQ
|
@claude review |
|
Claude finished @anna-parker's task in 1m 37s —— View job Review
All the previously raised review comments (mine, Copilot's, and the discussion with @anna-parker) have been addressed in the current state of the branch (commit
Cross-checked against upstream again: One very minor, non-blocking nit: the comment at I could not run the test suite locally — the |
Make ENA submission work with what prior raw reads validation has checked and guarantees:
file ending is one of
(".fastq.gz", ".fq.gz", ".fastq", ".fq")with arbitrary casing.Fix double compression bug
download_fastq_filesdecided whether to compress withfile_path.endswith((".gz", ".bz2")), which is case-sensitive. The upstream check in raw-reads-processing is not:_has_extensionlower-cases before comparing, soREADS.FASTQ.GZis a perfectly legal submission today. It reached this code, failed the.gztest, and got gzipped a second time.Make extensions lowercase so that webin is happy
ENA submission needs to lowercase extensions so that webin-cli accepts it (it compares suffixes with
String.endsWith, sor.FASTQ.GZis rejected outright with "Invalid FASTQ file suffix".Misnamed extension should be rejected in raw reads validation not here
A genuinely gzipped file named
reads.fastqstill gets compressed twice. The right place to catch it is at submission: separate PR #7266Worth deciding separately
If raw-reads-processing were narrowed to gzip-only the code would be simplified a lot. We would also save on storage, raw reads on Loculus/PPX would be more uniform and faster to download for everyone (and make upload for submitter faster). The trade-off is that submitters would have to create gzipped raw reads - but that's probably not an issue (and ENA requires gzip (or bz2) anyways so it's a normal requirement.
🚀 Preview: Add
previewlabel to enable