Skip to content

feat!(prepro): validate user-submitted external accessions - #7413

Open
maverbiest wants to merge 20 commits into
mainfrom
perpro-check-accessions
Open

maverbiest wants to merge 20 commits into
mainfrom
perpro-check-accessions

Conversation

@maverbiest

@maverbiest maverbiest commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

resolves #6859, #7250

Breaking change

Non-INSDC ingested entries that provide accessions of umbrella projects in bioprojectAccession will be rejected after this change. Also accessions that don't match the following patterns will error:

ACCESSION_PATTERNS: dict[EnaAccessionType, re.Pattern[str]] = {
    EnaAccessionType.BIOPROJECT: re.compile(r"PRJ[EDN][A-Z][0-9]+"),
    EnaAccessionType.BIOSAMPLE: re.compile(r"SAM[EDN][A-Z]?[0-9]+"),
    EnaAccessionType.RAW_READS: re.compile(r"[EDS]RR[0-9]{6,}"),
}

Summary

This PR adds a validation for values provided by direct submitters in the bioprojectAccession, biosampleAccession, and insdcRawReadsAccession fields (validation is skipped for INSDC-ingested data). This is done by checking if the provided accessions are public on ENA. The ENA checking implementation is based on the ENAVisibilityChecker in ena-submission/src/ena_deposition/check_external_visibility.py

New behaviour summary:

  • ENA unreachable after retries: add warning
  • Accession not public on ENA: add warning
  • Accession is unexpected format (e.g., bioproject format in biosample field): add error
  • Accession is an umbrella project: add error (we can't submit into this as broker, see Support of Umbrella bioprojects #6859)

Most of the LOC are from a parametrized test I asked Claude to add. I thought it was quite nice, but let me know if it's too exhaustive and I will cut it down.

Will this have a big impact on reprocessing?

This change means also that we will re-validate each non-INSDC-ingested value in these three fields when we do a full reprocessing of our data. Luckily, it seems we only have 448 direct-submission entries in the DB where one or more of these fields are set.

I found this out by running this query against the staging DB:

SELECT * FROM public.sequence_entries
WHERE group_id != 1 AND (
	COALESCE(submitted_data->'metadata'->>'bioprojectAccession', '') != ''
	OR COALESCE(submitted_data->'metadata'->>'biosampleAccession', '') != ''
	OR COALESCE(submitted_data->'metadata'->>'insdcRawReadsAccession', '') != ''
)
ORDER BY accession ASC, version ASC;

Naively, this is 448 * 3 = 1344 calls to ENA. Claude times these between 0.1 and 0.4 seconds, so ~538 seconds added to reprocessing all of PPX at most. This does not take into account caching, timeouts, and retries, though.

We could in theory record in the DB whether an accession has passed validation in the past so we never check it again, but this would be quite a big change for little gain IMO.

Screenshot

PR Checklist

  • All necessary documentation has been adapted.
  • The implemented feature is covered by appropriate, automated tests.
  • Any manual testing that has been done is documented (i.e. what exactly was tested?)
image

🚀 Preview: https://perpro-check-accessions.loculus.org

@claude claude Bot added deposition related to ENA/INSDC deposition deployment Code changes targetting the deployment infrastructure preprocessing Issues related to the preprocessing component labels Sep 24, 2026
@maverbiest
maverbiest force-pushed the perpro-check-accessions branch from ca74192 to feb5038 Compare September 27, 2026 12:03
@maverbiest
maverbiest marked this pull request as ready for review September 28, 2026 12:39
@claude

claude Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @maverbiest's task in 1m 3s —— View job


Code review: validate user-submitted external accessions

  • Read the PR diff and related code (RequestCache, call_function, tests)
  • Post inline comments
  • Post summary

Overall this is clean and easy to follow. The accession regexes, the INSDC-ingest bypass and the umbrella-project detection look correct. The parametrised tests cover the main paths well: wrong type, comma-separated values, case, 404, 5xx, retries exhausted and ingest bypass.

Main concern (inline): if ENA is down, the check can hold up the pipeline badly. Each lookup can take about 3.5 minutes because of 5 retries with a 30s timeout plus backoff. Failures aren't cached and there can be 3 lookups per entry, so a whole batch could stall even though the result is only a warning. I suggest fewer retries, a shorter timeout and a simple circuit breaker.

Other inline points:

  • An uncaught ET.ParseError becomes an internal error, which is inconsistent with how the other ENA failures are handled.
  • The swallowed RequestException should be logged.
  • Nit: strip whitespace instead of rejecting it.

General:

  • The preprocessing blocks are added to defaultOrganismConfig. This means every organism now calls ENA for these fields, including instances without ENA deposition. That may be intended; if not, consider gating it.
  • check_ena_accession hard-codes input_data.get("accession"). This matches the config, but a short docstring on the function would help people writing configs.
  • Missing test: a 200 response with malformed XML.
  • The PR checklist and manual testing notes are still empty.

# Successful ENA responses are small XML documents (a few KB). Cache hits refresh an entry's
# position; a bioproject shared across a batch stays cached while unique biosamples and runs
# cycle through.
ena_cache = RequestCache(max_size=16)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance when ENA is slow or down: this reuses RequestCache's default retries=5 with backoff_factor=1, and uses a 30s timeout. If ENA hangs or keeps returning 5xx/429, one lookup can block for about 6×30s plus ~31s of backoff, so roughly 3.5 minutes. Failed responses are never cached, and each entry can have up to 3 accessions, so every entry in the batch waits the full time again. An ENA outage could stall preprocessing for hours, even though the result is only a warning.

Suggestions:

  • Use fewer retries for this cache (e.g. RequestCache(max_size=16, retries=1)) and a shorter timeout (5–10s).
  • Add a small circuit breaker: after an "unreachable" result, skip ENA for N seconds and return the warning straight away.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll reduce retries and timeout as suggested, let's skip the circuit breaker for now, we can revisit if it's problematic

return RawProcessingResult(datum=accession)

def _is_umbrella_project(self, xml_bytes) -> bool:
for _, elem in ET.iterparse(io.BytesIO(xml_bytes), events=("start",)): # noqa: S314

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If ENA returns a 200 with a body that isn't valid XML (truncated body, an HTML maintenance page, etc.), ET.ParseError isn't caught here. call_function then turns it into an internal error, so the submission is blocked. Every other ENA problem only produces a warning. Consider catching ET.ParseError and returning the "unable to check" warning instead, and add a test case for it. Using defusedxml would also remove the need for the S314 noqa.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably a bit overboard to guard against ENA returning malformed XML on a 200, we also don't handle this specifically elsewhere

Comment thread preprocessing/nextclade/src/loculus_preprocessing/external_services.py Outdated
Comment thread preprocessing/nextclade/src/loculus_preprocessing/external_services.py Outdated
@maverbiest maverbiest changed the title feat(prepro): validate user-submitted external accessions feat!(prepro): validate user-submitted external accessions Sep 28, 2026
@maverbiest
maverbiest force-pushed the perpro-check-accessions branch from 2874df9 to c569c8a Compare October 19, 2026 07:34
RAW_READS = "raw_reads"


# Also guards against comma-separated lists, which the ENA browser API

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread preprocessing/nextclade/src/loculus_preprocessing/external_services.py Outdated
f"expected a value matching '{pattern.pattern}'."
)
try:
response = ena_cache.get_or_fetch(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... if the bioproject was once public and then is removed we cache the successful query... I guess in practice this is a small cache however as we probably wont get many bioprojects I wonder if this could be an issue

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add some time limit to the cache that makes entries stale after a while?

But then again, a bioproject could also be removed in the interim between PPX submission and ENA deposition, so we can't guard against this type of thing completely in any case

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I think its ok to ignore for now, just good to be aware (we might want to add a commetn)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also raises the question: what do we do if a bioproject exists when someone submits and when we propagate to ENA, but then three months later we bump the pipeline version and discover the bioproject has been deleted?

This would block reprossessing, right? What would we do in that situation, a DB surgery to remove the external accessions from the entry?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it would block reprocessing but I think we can just ignore that case for now as I think its unlikely (and tbf would be good for us to know if this happened lol)

Comment thread preprocessing/nextclade/src/loculus_preprocessing/external_services.py Outdated
Comment thread preprocessing/nextclade/src/loculus_preprocessing/external_services.py Outdated
Comment thread preprocessing/nextclade/src/loculus_preprocessing/external_services.py Outdated
@anna-parker anna-parker added the preview Triggers a deployment to argocd label Oct 19, 2026
accession: str
accession_type: str | None = "bioproject"
# How ENA responds: an HTTP status code, or an exception raised by the session.
# None means ENA must not be contacted at all.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is actually checked by checking if expected_url is actually null

Co-authored-by: Anna (Anya) Parker <50943381+anna-parker@users.noreply.github.com>

@anna-parker anna-parker left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is amazing thanks!

Before we merge this could you quickly confirm that for the existing bioproject, biosamples and runs that have been directly submitted to PPX none are not live? (should hopefully not be the case)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deployment Code changes targetting the deployment infrastructure deposition related to ENA/INSDC deposition preprocessing Issues related to the preprocessing component preview Triggers a deployment to argocd

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support of Umbrella bioprojects

2 participants