Skip to content

manifest

manifest

Dataset manifests: declaration, discovery, and model-level fetching.

Datasets are declared in TOML manifests. fwl-io ships one manifest for datasets shared by several models (spectral files, multi-consumer lookup tables); each model package may ship its own manifest for data only it consumes and expose it through the fwl_io.manifests entry-point group. Adding data to a model therefore never requires an fwl-io release.

Manifest schema, one table per dataset, identified by its zenodo key::

manifest_schema = 1                     # optional, see below

[interior.eos.wolf_bower_2018]
name = "Wolf & Bower (2018) MgSiO3 equation of state"
zenodo = "10.5281/zenodo.1234567"       # version DOI, never a concept DOI
dataverse = "10.34894/ABCDEF"           # optional download mirror
required_by = ["aragog", "zalmoxis", "spider"]
extract = "tar"                         # optional: unpack a single-archive deposit

The optional root manifest_schema names the schema the file was written against. A manifest that declares one is held to it: only the schema the installed fwl-io implements is accepted, a higher number meaning the reader is too old and a lower one meaning the manifest was written for a schema that stopped loading when the number rose. That is what sharpens the diagnosis elsewhere, since an unknown field in such a manifest can only be a misspelling.

The dotted table key is the dataset location below the data root: the table above resolves into interior/eos/wolf_bower_2018. Key segments are restricted to letters, digits, _ and -, each starting with a letter, digit or _, so a key can neither escape the data root nor split into an unintended path depth. A dataset resolves into <key-as-path>/r<record-id>, the version directory named for its Zenodo record.

A deposit packaged as one archive declares extract = "tar" or "zip"; its registry lists the archive, and the fetcher downloads and checksum-verifies it, then extracts the members into the dataset directory (the archive is not kept).

Every dataset requires a Zenodo version DOI: the committed registry is generated from the Zenodo record, so Dataverse is a download mirror, not an alternative primary source. Version DOIs are pinned deliberately: a Zenodo concept DOI resolves to the newest deposit and would let data drift underneath pinned code, so fwl-io sync rejects concept DOIs.

Every dataset has a committed registry file next to the manifest, named <dotted-key>.registry.txt, generated by fwl-io sync. Packages that ship their own manifest must ship the registry files with it (include both in package-data).

Dataset(*, key, name, zenodo=None, dataverse=None, required_by=tuple(), registry_path=None, extract=None) dataclass

A single downloadable dataset declared in a manifest.

subdir property

Dataset location below the data root, derived from the dotted key.

registry()

Return the committed name-to-hash registry for this dataset.

Source code in src/fwl_io/manifest.py
234
235
236
237
238
239
240
def registry(self) -> dict[str, str]:
    """Return the committed name-to-hash registry for this dataset."""
    if self.registry_path is None or not self.registry_path.is_file():
        raise FileNotFoundError(
            f'no registry file for dataset {self.key!r}; run: fwl-io sync <manifest>'
        )
    return load_registry(self.registry_path)

ManifestSchemaError

Bases: ValueError

A manifest and the installed fwl-io disagree about the manifest schema.

Raised when a manifest declares something this fwl-io does not understand, something it no longer understands, or a field it reads only inside a dataset table. Subclasses ValueError, so callers that already handle a malformed manifest keep working.

discover_manifests()

Collect datasets from every installed fwl_io.manifests entry point.

Entry points must resolve to a zero-argument callable returning the manifest path. A provider whose manifest fails to load is skipped with a logged warning, so one broken package cannot break data access for every other model.

Source code in src/fwl_io/manifest.py
436
437
438
439
440
441
442
443
444
445
def discover_manifests() -> dict[str, list[Dataset]]:
    """Collect datasets from every installed ``fwl_io.manifests`` entry point.

    Entry points must resolve to a zero-argument callable returning the
    manifest path. A provider whose manifest fails to load is skipped with a
    logged warning, so one broken package cannot break data access for every
    other model.
    """
    found, _ = _discover()
    return found

fetch_for(model, data_root=None)

Fetch every dataset a given model requires; return paths per dataset.

All matching datasets are attempted; failures are collected and raised together at the end so one broken dataset does not block the others (files fetched before the error remain in place).

An unreadable manifest counts as a failure here, even when other datasets arrived: a manifest that cannot be parsed may be the one declaring this model, and a model is routinely served by both its own manifest and the shared one. Listing is more forgiving, since it reports per provider and a reader can see which one is missing.

Parameters:

Name Type Description Default
model str

Model name matched (case-insensitively) against required_by.

required
data_root str | Path | None

Override for the data root; defaults to the resolved FWL_DATA tree.

None
Source code in src/fwl_io/manifest.py
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
def fetch_for(model: str, data_root: str | Path | None = None) -> dict[str, list[Path]]:
    """Fetch every dataset a given model requires; return paths per dataset.

    All matching datasets are attempted; failures are collected and raised
    together at the end so one broken dataset does not block the others
    (files fetched before the error remain in place).

    An unreadable manifest counts as a failure here, even when other datasets
    arrived: a manifest that cannot be parsed may be the one declaring this
    model, and a model is routinely served by both its own manifest and the
    shared one. Listing is more forgiving, since it reports per provider and
    a reader can see which one is missing.

    Parameters
    ----------
    model : str
        Model name matched (case-insensitively) against ``required_by``.
    data_root : str | Path | None
        Override for the data root; defaults to the resolved FWL_DATA tree.
    """
    from fwl_io.fetch import create_fetcher

    model = model.lower()
    fetched: dict[str, list[Path]] = {}
    failures: dict[str, str] = {}
    providers, provider_errors = _discover()
    for datasets in providers.values():
        for ds in datasets:
            if model not in tuple(r.lower() for r in ds.required_by):
                continue
            try:
                fetcher = create_fetcher(
                    subdir=ds.subdir,
                    zenodo=ds.zenodo,
                    dataverse=ds.dataverse,
                    registry=ds.registry(),
                    data_root=data_root,
                    extract=ds.extract,
                )
                fetched[ds.key] = fetcher.fetch_all()
            except Exception as exc:  # noqa: BLE001 -- aggregate and re-raise below
                failures[ds.key] = str(exc)
    if failures or provider_errors:
        report = [f'fetching data for model {model!r} failed ({len(fetched)} dataset(s) arrived)']
        if failures:
            report.append(f'{len(failures)} dataset(s) failed:')
            report += [f'  {key}: {msg}' for key, msg in sorted(failures.items())]
        if provider_errors:
            report.append(f'{len(provider_errors)} manifest(s) could not be read:')
            report += [f'  {name}: {msg}' for name, msg in sorted(provider_errors.items())]
        raise RuntimeError('\n'.join(report))
    return fetched

load_manifest(path)

Load and validate all datasets declared in one manifest file.

Source code in src/fwl_io/manifest.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
def load_manifest(path: str | Path) -> list[Dataset]:
    """Load and validate all datasets declared in one manifest file."""
    path = Path(path)
    with path.open('rb') as fh:
        tree = tomllib.load(fh)

    # Read the declaration first: a manifest above this code's schema cannot be
    # judged by this code's rules, so it must be refused before they are applied.
    declared_schema = _read_declared_schema(tree)
    _reject_declared_subdir('the manifest root', tree)
    datasets: list[Dataset] = []
    folded: dict[str, str] = {}
    for key, table in _walk_tables(tree, declared_schema=declared_schema):
        clash = folded.setdefault(key.lower(), key)
        if clash != key:
            raise ValueError(
                f'datasets {clash!r} and {key!r} differ only in case; they would share one '
                f'directory and one registry file on a case-insensitive filesystem'
            )
        zenodo = table.get('zenodo')
        dataverse = table.get('dataverse')
        if zenodo == '':
            raise ValueError(
                f'dataset {key!r}: the "zenodo" value is empty; a Zenodo version DOI is '
                f'required ("dataverse" is a download mirror, not a primary source)'
            )
        if not isinstance(zenodo, str) or not ZENODO_DOI_PATTERN.fullmatch(zenodo):
            raise ValueError(
                f'dataset {key!r}: zenodo value {zenodo!r} is not a Zenodo DOI '
                f'of the form 10.5281/zenodo.<record-id>'
            )
        if dataverse is not None and (
            not isinstance(dataverse, str) or not _GENERIC_DOI_PATTERN.fullmatch(dataverse)
        ):
            raise ValueError(f'dataset {key!r}: dataverse value {dataverse!r} is not a DOI')
        name = table.get('name', key)
        if not isinstance(name, str) or not name.strip():
            raise ValueError(f'dataset {key!r}: "name" must be non-empty text, got {name!r}')
        required_by = table.get('required_by', ())
        if not isinstance(required_by, list | tuple) or not all(
            isinstance(model, str) for model in required_by
        ):
            raise ValueError(
                f'dataset {key!r}: "required_by" must be a list of model names, got {required_by!r}'
            )
        extract = table.get('extract')
        if extract is not None and extract not in ARCHIVE_KINDS:
            raise ValueError(
                f'dataset {key!r}: extract value {extract!r} must be one of {ARCHIVE_KINDS}'
            )
        datasets.append(
            Dataset(
                key=key,
                name=name,
                zenodo=zenodo,
                dataverse=dataverse,
                required_by=tuple(required_by),
                registry_path=path.parent / f'{key}.registry.txt',
                extract=extract,
            )
        )
    return datasets

shared_manifest_path()

Entry-point target: the manifest of datasets shared across models.

Source code in src/fwl_io/manifest.py
417
418
419
def shared_manifest_path() -> Path:
    """Entry-point target: the manifest of datasets shared across models."""
    return Path(__file__).parent / 'data' / 'shared_manifest.toml'

zenodo_record_id(doi)

Return the numeric record id of a Zenodo version DOI.

Parameters:

Name Type Description Default
doi str

A Zenodo DOI of the form 10.5281/zenodo.<record-id> (an optional doi: prefix is accepted).

required

Returns:

Type Description
str

The trailing record-id digits.

Raises:

Type Description
ValueError

When the string is not a Zenodo DOI.

Source code in src/fwl_io/doi.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def zenodo_record_id(doi: str) -> str:
    """Return the numeric record id of a Zenodo version DOI.

    Parameters
    ----------
    doi : str
        A Zenodo DOI of the form ``10.5281/zenodo.<record-id>`` (an optional
        ``doi:`` prefix is accepted).

    Returns
    -------
    str
        The trailing record-id digits.

    Raises
    ------
    ValueError
        When the string is not a Zenodo DOI.
    """
    match = ZENODO_DOI_PATTERN.fullmatch(doi.strip())
    if not match:
        raise ValueError(f'{doi!r} is not a Zenodo DOI of the form 10.5281/zenodo.<id>')
    return match.group(2)