Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions docs/architecture/integration-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,16 @@ was built first as the reference and the rest mirror it.
- **Shared helpers removed duplication.** `metaseed._http.request_json`
(retry/backoff) and `metaseed.isatab.to_isatab` are written once and reused by
all the relevant adapters.
- **Schema-valid output where it counts.** The ENA exporter's study/sample/run
XML **validate against ENA's official SRA XSDs** — i.e. the output is not just
well-formed, it would be accepted.
- **Every exporter has a conformance test.** The ENA export **validates against
ENA's official SRA XSDs** (external schema — the gold standard); the PRIDE,
MetaboLights, and BrAPI exports have structural/shape conformance tests
(required px `MTD`/`FMH`/`FME` lines, the ISA-Tab sections + study/assay/MAF
files, and the BrAPI v2 object shape via jsonschema). All are `@network`
round-trips against a real record.
- **Importers tested across diverse accessions.** Each importer was run against
several real, varied accessions (ENA run/sample/study incl. a 495-file study;
four PXD projects incl. a 2384-file one; four MTBLS studies) without surfacing
new mapping gaps.

## What didn't work (and the lessons)

Expand Down Expand Up @@ -90,9 +97,9 @@ was built first as the reference and the rest mirror it.

## Honest status

The eight adapters are a real first step, **not production-validated.** Each was
smoke-tested against a single live record (which already caught real bugs), and
the clients now page and retry. Still open: broader accession coverage, export
validation for BrAPI/PRIDE/MetaboLights (ENA is XSD-valid), and verification that
exports are accepted by the live submission systems. Tracking: umbrella #75,
importers epic #76.
The eight adapters now page and retry, each importer has been exercised across
several diverse live accessions, and every exporter has a conformance test
(ENA against the official XSD; the rest structural/shape). The one remaining
unknown is **acceptance by the live submission systems** — no export has yet
been round-tripped through a real ENA/PRIDE/MetaboLights/BrAPI submission
endpoint. Tracking: umbrella #75, importers epic #76.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dev = [
"httpx>=0.27",
"jupyter>=1.0",
"xmlschema>=3.0",
"jsonschema>=4.0",
]
docs = [
"mkdocs>=1.6,<2.0",
Expand Down
42 changes: 42 additions & 0 deletions tests/test_brapi/test_conformance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Conformance of the BrAPI export to the BrAPI v2 object structure.

Uses jsonschema to assert the v2 shape: every object carries its DbId, and an
observation unit nests its level inside observationUnitPosition (never a
top-level ``observationLevel`` — the bug fixed in this adapter). Opt-in (network).
"""

from __future__ import annotations

import pytest
from jsonschema import validate

from metaseed.brapi import import_brapi, to_brapi

BASE_URL = "https://test-server.brapi.org/brapi/v2"

_OU_SCHEMA = {
"type": "object",
"required": ["observationUnitDbId"],
"not": {"required": ["observationLevel"]}, # must be nested, not top-level
"properties": {
"observationUnitPosition": {
"type": "object",
"properties": {"observationLevel": {"type": "object"}},
}
},
}
_REQUIRED_ID = {
"studies": "studyDbId",
"germplasm": "germplasmDbId",
"observationUnits": "observationUnitDbId",
}


@pytest.mark.network
def test_brapi_export_matches_v2_shape():
bodies = to_brapi(import_brapi(BASE_URL))
for collection, id_field in _REQUIRED_ID.items():
for obj in bodies.get(collection, []):
assert id_field in obj, f"{collection} object missing {id_field}"
for unit in bodies.get("observationUnits", []):
validate(instance=unit, schema=_OU_SCHEMA)
46 changes: 46 additions & 0 deletions tests/test_metabolights/test_conformance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Conformance of the MetaboLights export to the ISA-Tab structure.

Structural validation (ISA-Tab has no installable schema here): the investigation
file must carry the required ISA sections, and the bundle must include the study,
assay, and MAF files with their mandatory headers. Opt-in (network).
"""

from __future__ import annotations

import pytest

from metaseed.metabolights import import_accession, to_metabolights

_REQUIRED_SECTIONS = (
"ONTOLOGY SOURCE REFERENCE",
"INVESTIGATION",
"STUDY",
"STUDY FACTORS",
"STUDY ASSAYS",
"STUDY PROTOCOLS",
"STUDY CONTACTS",
)


def _isatab_violations(docs: dict[str, str]) -> list[str]:
errors = []
inv = docs.get("i_Investigation.txt", "")
errors += [f"missing section {s}" for s in _REQUIRED_SECTIONS if s not in inv]
if "Investigation Identifier" not in inv:
errors.append("no Investigation Identifier row")
if not [n for n in docs if n.startswith("s_")]:
errors.append("no study (s_) file")
if not [n for n in docs if n.startswith("a_")]:
errors.append("no assay (a_) file")
if not [n for n in docs if n.startswith("m_")]:
errors.append("no MAF (m_) file")
for name in (n for n in docs if n.startswith("s_")):
if "Sample Name" not in docs[name].splitlines()[0]:
errors.append(f"{name} missing 'Sample Name' header")
return errors


@pytest.mark.network
def test_metabolights_export_is_isatab_conformant():
docs = to_metabolights(import_accession("MTBLS1"))
assert _isatab_violations(docs) == []
55 changes: 55 additions & 0 deletions tests/test_pride/test_conformance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Conformance of the PRIDE ``submission.px`` export to the px-submission format.

Structural validation: PRIDE has no external schema (unlike ENA's SRA XSD), so
this asserts the px-submission-tool structure — the required ``MTD`` metadata
lines and a well-formed ``FMH``/``FME`` file-mapping section. Opt-in (network):
exports a real project and checks the result would be structurally accepted.
"""

from __future__ import annotations

import pytest

from metaseed.pride import import_accession, to_pride_submission

# MTD keys the px-submission tool requires for a project.
_REQUIRED_MTD = (
"project_title",
"project_description",
"submitter_name",
"submitter_email",
"submission_type",
)


def _px_violations(text: str) -> list[str]:
lines = [ln for ln in text.splitlines() if ln]
mtd = {
parts[1]
for ln in lines
if (parts := ln.split("\t"))[0] == "MTD" and len(parts) >= 3
}
errors = [f"missing MTD {k}" for k in _REQUIRED_MTD if k not in mtd]
if "species" not in mtd:
errors.append("no species line")
if "instrument" not in mtd:
errors.append("no instrument line")

if not any(ln.startswith("FMH\t") for ln in lines):
errors.append("missing FMH header")
fme = [ln for ln in lines if ln.startswith("FME\t")]
if not fme:
errors.append("no FME file entries")
for ln in fme:
cols = ln.split("\t")
if len(cols) != 4:
errors.append(f"FME has {len(cols)} columns, expected 4: {ln!r}")
elif not cols[1].isdigit():
errors.append(f"FME file_id not numeric: {ln!r}")
return errors


@pytest.mark.network
def test_pride_export_is_px_conformant():
px = to_pride_submission(import_accession("PXD000001"))["submission.px"]
assert _px_violations(px) == []
Loading