Repository navigation
Development Provider Framework
Infrastructure shared by every npdb convert bagel <provider> command: how a provider manager is built, what happens to downloaded files before conversion, the cache rule, credential loading, and how to add a provider. Per-provider usage lives in Providers.
sequenceDiagram
actor User
participant CLI as npdb convert bagel
participant Factory as ProviderManagerFactory
participant Provider as Provider manager
participant Prep as Shared preparation
participant Pipeline as Local conversion pipeline
participant Output as Output directory
User->>CLI: provider + identifier + options
CLI->>Factory: create(provider, credentials, cache)
Factory-->>CLI: manager
CLI->>Provider: fetch(identifier, staging path)
Provider->>Provider: Download provider files
Provider->>Prep: prepare_fetched(staging root)
Prep->>Prep: unpack archives, normalize BIDS layout
Prep-->>Provider: prepared BIDS root
Provider-->>CLI: prepared dataset path
CLI->>Pipeline: local2bagel(dataset, options)
Pipeline->>Output: validation report, ledger, JSON-LD
Output-->>User: generated dataset files
- The fetch directory is
<output>/<provider>_<dataset_id>;dataset_idis derived from the identifier (/and.replaced by_; git and MIDRC compute their own). -
fetchreturns the prepared dataset path. The CLI no longer unpacks or prepares. - Providers that collect several components (Figshare collection articles, MIDRC jobs, Mendeley files) accumulate them into one dataset root and prepare it once, after the whole loop.
- MIDRC and Mendeley downloads show a live progress display (download observers).
- After fetch, the pipeline is identical to
npdb convert bagel local.
| Path | Role |
|---|---|
src/npdb/managers/model.py |
ProviderName enum, ProviderManager base (fetch, unpack, prepare, prepare_fetched, describe, ensure_cache_dir), GitManager (sparse clone, auth headers). |
src/npdb/managers/preparation.py |
unpack_provider_archives, extract_archive, prepare_provider_dataset. |
src/npdb/managers/<provider>.py |
One manager per provider. |
src/npdb/factories.py |
ProviderManagerFactory.create (reads credentials from the environment), GiteaManagerFactory. |
src/npdb/cli/cli.py |
One @bagel.command per provider, all calling _provider_call. |
ProviderManager.unpack scans the download for ZIP and TAR archives (including gzip, bzip2 and xz compressed TAR):
- Each archive is extracted into the directory containing it and deleted only after successful extraction.
- Member paths are validated; unsafe paths fail extraction.
- Ordinary files are unchanged.
- Archives found inside newly extracted content are not recursively unpacked.
ProviderManager.prepare first selects the dataset root, then normalizes common layouts into a BIDS root:
- If the staging root has no
dataset_description.jsonand nosub-*entry, the shallowest nested folder that looks like a dataset (participants.tsv,rawdata/orsub-*folders) is used instead. This handles archives wrapped in a folder such as<repo>-<commit>/(Zenodo GitHub releases, Mendeley). Hidden folders,__MACOSXand the auxiliary folders below are ignored; several candidates raise an error. - If no
derivatives/exists, non-hidden top-level directories other thanrawdata,sub-*,sourcedata,stimuli,codeandphenotypeare moved into a newderivatives/. - The contents of
rawdata/are moved to the dataset root and the emptyrawdata/is removed. - If
participants.tsvis missing, it is generated withparticipant_id,ageandsex, from the uniquesub-<label>entities in raw-data file and directory names. Age and sex areN/A. Existing tables are preserved.
Providers can override unpack/prepare (MIDRC converts DICOMs and builds derivatives, see MIDRC).
Providers that download large archives or bundles use a persistent local cache:
-
--cache-diron the CLI, otherwiseNP_NPDB_CACHE_DIR. - A manager with
requires_cache = True(Kaggle), or one that callsensure_cache_dir(required=True)(MIDRC Discovery), fails with: A cache directory is required for this provider. Pass --cache-dir or set NP_NPDB_CACHE_DIR. - This avoids silently downloading very large archives into a temporary directory that may be cleaned up, or that fills the system disk.
export NP_NPDB_CACHE_DIR="$HOME/.cache/npdb-downloads"ProviderManagerFactory.create reads provider credentials from process environment variables (NP_GIT_*, NP_MENDELEY_ACCESS_TOKEN, NP_MIDRC_*, NP_ZENODO_TOKEN, NP_FIGSHARE_TOKEN); CLI options (--token, --credentials-path, --endpoint) take precedence.
Important
.env is loaded automatically only by npdb download and npdb convert bagel gitea. For the other providers, export it first:
set -a; source .env; set +aVariable template: template.env.
Add one provider end to end: its command appears in help without the optional library installed, fetches into the standard staging directory, returns a prepared BIDS root, and writes Neurobagel JSON-LD. Keep tests offline by mocking only the provider's network/client boundary.
Before coding, write down the backend contract. This determines which shared APIs fit and where provider-specific work is needed.
| Question | Decide |
|---|---|
| Identifier | What exact ID, URL, DOI, or manifest does identifies the dataset? |
| Access | Which data is public/private? What credential is needed, and how is it supplied? |
| Payload | Does the source return files, archives, a manifest, or multiple parts? |
| BIDS readiness | Is the payload already BIDS? If not, what transformation produces a valid BIDS root? |
| Storage | Can a download require a persistent cache? |
| Provenance | What stable URL and access type should appear in the Neurobagel output? |
Shared preparation handles common archives and layouts; it cannot infer subject metadata or convert arbitrary data into BIDS. If the source is not BIDS, implement and test a provider-specific transformation or clearly reject that layout.
Use existing providers as references for techniques, not templates to duplicate wholesale:
| Technique | Reference |
|---|---|
| Lazy optional-client import, one download call | OpenNeuro manager |
| HTTP API, redirects, collection files | Figshare manager |
| Integrity checks, cache, authentication fallback | Mendeley manager |
| Authenticated manifests and specialized BIDS assembly | MIDRC manager |
| Git repository | Git manager |
ProviderName is the canonical internal key used by the factory and CLI. Add the backend's lowercase key there:
class ProviderName(str, Enum):
# existing names ...
EXAMPLE = "example"Use that key for the CLI subcommand, manager provider_name, and dependency extra. The wiki page may use a readable display name, but follow the Development-Provider-<Name> filename convention.
Create src/npdb/managers/example.py as a subclass of ProviderManager. Its contract is: accept an identifier and staging path, fetch source files there, and return the prepared BIDS root. The base prepare_fetched() runs unpacking and common layout preparation exactly once.
from pathlib import Path
from typing import Any, ClassVar
from npdb.extras import require_module
from npdb.managers.model import ProviderManager, ProviderName
class ExampleProviderManager(ProviderManager):
provider_name: ClassVar[ProviderName] = ProviderName.EXAMPLE
def __init__(self, token: str | None = None, cache_dir: str | Path | None = None):
super().__init__(cache_dir=cache_dir)
self.token = token
def fetch(self, identifier: str, output_dir: str | Path, **_: Any) -> Path:
client = require_module(
"example_client", extra="example", feature="The Example provider"
)
output = Path(output_dir)
output.mkdir(parents=True, exist_ok=True)
client.download(
identifier=identifier,
output_dir=str(output),
token=self.token,
)
return self.prepare_fetched(output)- The module, token, and method names above are placeholders; match the real client.
- Keep optional imports inside
fetch()usingrequire_module()sonpdb --helpand other backends work in limited installs. - Use
require_executable()for required system programs. - If every fetch requires a cache, set
requires_cache = True; if only some requests do, callself.ensure_cache_dir(required=True)for those inputs. - Use the base
unpack()andprepare()unless the provider needs a documented exception. - For collections or APIs returning several archives, download all components into one root, then call
prepare_fetched()once after the loop. Do not prepare each component separately.
Add an optional extra in pyproject.toml:
[project.optional-dependencies]
example = ["example-client>=1.0"]
all = [
"npdb[git,gitea,kaggle,mendeley,figshare,midrc,openneuro,zenodo,example]",
]If the backend uses only core dependencies or system programs, use example = [] so npdb[example] is valid. Update the existing all list, then regenerate the lockfile:
uv lock
uv sync --active --extra exampleIf the manager uses credentials, add their names and comments, not secrets, to template.env. ProviderManagerFactory.create reads environment variables and CLI values, then constructs the manager. Add a branch like this, adapting constructor fields to the manager:
if provider_name == ProviderName.EXAMPLE:
return ExampleProviderManager(
token=token or os.environ.get("NP_EXAMPLE_TOKEN"),
cache_dir=cache_dir,
)The token value above is already a factory argument used by Zenodo and Figshare. If the new provider needs a distinct CLI option, add it to create() and _provider_call(). Keep secrets out of command echo, logs, exception text, and test output.
In src/npdb/cli/cli.py, add a @bagel.command("example") beside the provider commands. The OpenNeuro command shows the exact relationship: Typer arguments/options become values passed to _provider_call(), which performs shared validation, repair, annotation, ledger, and JSON-LD export.
After declaring the command's Typer arguments and options (see the complete OpenNeuro command), pass their values into _provider_call():
_provider_call(
"example",
dataset,
output=output,
online_url=f"https://example.org/datasets/{dataset}",
access_type="public",
mode=mode,
phenotype_dict=phenotype_dict,
headless=headless,
timeout=timeout,
artifacts_dir=artifacts_dir,
ai_provider=ai_provider,
ai_model=ai_model,
header_map=header_map,
extend_modalities=extend_modalities,
bids_version=bids_version,
suffix_alias=suffix_alias,
bids_severity_threshold=bids_severity_threshold,
cache_dir=cache_dir,
token=token,
)In this example, those names are the values declared by Typer in the command signature. Use the provider's canonical URL and access type, not the placeholder values above. If the factory needs a new provider-specific value, pass it through _provider_call() to ProviderManagerFactory.create() as well.
Use monkeypatch or unittest.mock.patch at the network/client boundary. Never call the live provider, and never require credentials in CI. A provider is not finished when a mocked manager unit test passes: the shared acceptance test must take a tiny downloaded BIDS fixture through preparation and the real JSON-LD conversion path.
-
Manager unit tests: mock the client; assert identifier, credentials, and output path; test success and anticipated failures. For multi-file providers, assert all files share one staging root and
prepare_fetched()runs once after all downloads. -
Preparation tests: use tiny synthetic archives/layouts. Cover wrapped roots, missing
participants.tsv, path collisions, unsafe archive members, and any provider-specific transformation. The shared archive rules are tested intest/test_provider_archives.py. -
CLI conversion test: invoke
npdb convert bagel example ...; mock the remote fetch and browser annotation, but assert the prepared BIDS root reaches the conversion pipeline and a JSON-LD file is written. See the synthetic end-to-end test intest/test_midrc.py. -
Limited-install test: add the module/extra/manager case to
test/test_extras.py. Confirm the package is imported lazily, missing-extra guidance namesnpdb[example], andnpdb --helpstill works. -
Provider adequacy test: add a row to the
PROVIDERStable intest/test_provider_adequacy.py. Its parametrized checks cover ingestability, missing configuration, HTTP/network failures, and non-BIDS payloads. Only the fetch boundary is mocked; shared unpacking, preparation, validation, and JSON-LD conversion run for real.
Useful existing patterns: shared preparation, Figshare client mocking, Mendeley integrity/cache cases, and MIDRC download + conversion.
Put provider-specific unit cases in a new test/test_example.py. After adding the provider to the shared tests, run the focused checks first, then the offline full suite:
.venv/bin/python -m pytest -q test/test_example.py test/test_provider_adequacy.py test/test_extras.py
.venv/bin/python -m pytest -qAdd Development-Provider-Example.md using the same Install → Access → Download → Details structure as the other provider pages. Then add the new backend to each index:
-
Development-Providers.md: comparison-table row with identifier, extra, credentials, and cache requirements. -
Development.md: provider link in its provider list. -
_Sidebar.md: link in the provider section. -
Development-CLI-Reference.md: command, argument names, and provider-specific options.
Keep README.md and user docs/ focused on common user workflows. A conversion backend alone does not add support to npdb download; that command has its own TSV-driven backend selection and requires a separate implementation and test change.
Using data
Tutorials
Infrastructure