Integration Adapters: Approach and Decisions¶
metaseed is a two-way bridge to scientific data repositories: importers pull public metadata into a validated profile dataset, and exporters render a dataset back into a repository's submission format. This page records the approach, what worked, what didn't, and why these choices were made — so the next adapter (and the next contributor) starts from the rationale, not a blank page.
The shape¶
Every adapter follows one seam, in one of two directions:
- Import:
accession/server → fetch metadata → map (spec-driven) → validate → dataset - Export:
dataset → render → submission documents
Each repository lives in its own package (metaseed.<repo>) with at most three
parts: a client (network I/O), a mapper (pure import logic), and an export
(pure render logic). ENA, BrAPI, PRIDE, and MetaboLights all implement this; ENA
was built first as the reference and the rest mirror it.
| Repo | Import target | Export format |
|---|---|---|
| ENA | ena profile |
SRA submission XML |
| BrAPI | miappe profile |
BrAPI v2 JSON |
| PRIDE | pride profile |
submission.px + SDRF |
| MetaboLights | metabolights profile |
ISA-Tab + MAF |
The action registry¶
metaseed.adapters is the single declarative list of what each adapter offers.
An Action names a kind (import, export, push), a lazy
"module:function" ref, and a surface telling a host where to group its
control. actions_for_profile(profile, kind=..., surface=...) is the only call a
host needs: a new adapter capability appears in the web UI and in the hub by
declaring itself here, never by editing a host.
| Action | Kind | Profile | Takes |
|---|---|---|---|
ena-import |
import | ena |
ENA accession |
pride-import |
import | pride |
ProteomeXchange accession |
metabolights-import |
import | metabolights |
MetaboLights study accession |
brapi-import |
import | miappe |
BrAPI v2 server URL |
ena |
export | ena |
— |
pride |
export | pride |
— |
metabolights |
export | metabolights |
— |
Every import action takes exactly one string, so a host renders one text input
and calls action.resolve()(value). What that string means differs — an
accession for the three archives, a server URL for BrAPI — so each action
carries an input_label and input_placeholder for the prompt. Without them a
host would have to hard-code per-adapter wording, which is the coupling the
registry exists to remove.
Running an import¶
Resolving the action is only half of an import: the returned MetaseedClient
has to replace what the host is editing. metaseed.ui.datasets.import_from_source
does both — it looks the action up with adapters.import_action_for_profile,
calls it, installs the resulting facade on the AppState, and returns the
imported profile, version and root count. Every host in this repository goes
through it, so an importer cannot behave differently depending on where it was
started from.
Two failures are distinguished, because the fix differs:
NoImporterError— the profile declares no import action. The message lists the profiles that do.EmptyImportError— the importer ran and returned nothing. The accession is wrong or has no public metadata; the dataset is left untouched.
ModuleNotFoundError is left to propagate: it means the adapter's extra
(metaseed[pride]) is not installed, which no host can resolve on the user's
behalf.
The hosts¶
| Host | Entry point |
|---|---|
| Web UI | POST /import/source with key and value form fields; the control is rendered on the dataset page from import_options_for_profile |
| MCP | the import_from_database tool (profile, accession, name) |
| Library | metaseed.<repo>.import_accession(...) directly |
The web UI renders one import control per action the active profile offers, next to the adapter exports, and saves nothing until the import succeeds. The MCP tool additionally saves the result under a dataset name, because an agent has no page to leave the result sitting on.
Decisions (and why)¶
- Optional extras, not core deps. Each adapter installs with
metaseed[<repo>](its network/parse deps), so core stays lean and single-user-foundation-focused. Importingmetaseed.<repo>pulls in no web stack — a guard test enforces this. Consumers take only what they need. - Spec-driven mapping. The existing profiles are the contract; adapters map
to/from profile entities rather than inventing a parallel schema. The
metaseed.isatabwriter is shared by themetabolightsandisaprofiles (and is the head start for FAIRDOM-SEEK) because ISA-Tab is their common backbone. - Reference-adapter-first. Build one vertical (ENA) end to end, prove the seam, then replicate. This made the other three cheap — built largely by parallel agents copying the ENA files.
- Metadata, not data. Adapters reference raw files (FASTQ, RAW/mzML, spectra) by URL/name; they never download them. "Import all of an accession" means all of its metadata.
- Pure mappers/writers + injectable clients. The mapper and exporter take
plain dicts / a
MetaseedClientand touch no network, so they are unit-tested from fixtures. The client accepts an injectedhttpx.Client, so request shaping is tested with a mock transport. The network call itself is one@network-marked smoke test. - Lenient build. Entities are created with
skip_validationduring import, so a record missing a field never aborts the import;client.validate()reports gaps afterward.
What worked¶
- The seam replicated cleanly. Six parallel subagents (in isolated git worktrees) built 7 of the 8 adapters by mirroring the ENA reference; each was then reviewed and live-tested by hand.
- Shared helpers removed duplication.
metaseed._http.request_json(retry/backoff) andmetaseed.isatab.to_isatabare written once and reused by all the relevant adapters. - 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/FMElines, the ISA-Tab sections + study/assay/MAF files, and the BrAPI v2 object shape via jsonschema). All are@networkround-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)¶
- Hermetic tests passed against wrong assumptions. Agents wrote fixtures encoding guessed API shapes; the hermetic tests then passed trivially. Live data exposed the gaps:
- MetaboLights records people/publications on the study, not the
investigation — the importer read the (empty) investigation level and
silently dropped every study's contributors. Caught only by importing real
MTBLS1. Fixed; the fixture now matches the real API and guards it. - The PRIDE dataset is a single composed
Datasetwith nested lists, not flat ref-linked entities — inspecting the flat entity list looked empty until we read the nested fields. - Lesson: a live smoke test per adapter is non-negotiable; fixtures must be derived from real responses, not invented.
- Pagination was silently dropped. The first cut of the PRIDE and BrAPI clients fetched a single page, truncating large datasets (PXD000561: 100 of 2384 files). Only visible by testing a large accession. Both now page through; tests guard it.
- A subagent hit a session limit mid-task. The ISA-Tab exporter agent stopped after writing the writer; the work was salvaged and finished by hand.
- The commit hook can silently abort.
ruff formatrunning in pre-commit can reformat a staged file and abort the commit while a script's echo claims success — always verifygit logand pre-format before committing.
Honest status¶
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.