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
files = ["eos.dat", "grid.dat"]         # optional: use only these files of the record

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).

A deposit that carries more than the dataset needs declares files, a list of file names within the record. The registry, and with it fetch, check, relocate and the Dataverse mirror, then covers only those files; without files the whole record is the dataset. files and extract cannot be combined.

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, files=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.

A dataset that declares files must have a registry naming exactly those files, so a registry left over from before the list changed is refused rather than fetched in part or in excess.

Source code in src/fwl_io/manifest.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
def registry(self) -> dict[str, str]:
    """Return the committed name-to-hash registry for this dataset.

    A dataset that declares ``files`` must have a registry naming exactly
    those files, so a registry left over from before the list changed is
    refused rather than fetched in part or in excess.
    """
    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>'
        )
    entries = load_registry(self.registry_path)
    if self.files is not None and set(entries) != set(self.files):
        extra = sorted(set(entries) - set(self.files))
        absent = sorted(set(self.files) - set(entries))
        raise ValueError(
            f'the registry of dataset {self.key!r} does not match its "files" list '
            f'(in the registry only: {extra}; in "files" only: {absent}); '
            f'run: fwl-io sync <manifest>'
        )
    return entries

ErrorKind

Bases: Enum

Why a manifest provider was left out of discovery.

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.

ProviderError(kind, message, models=frozenset()) dataclass

A provider left out of discovery: the reason, the message, and for a conflict the lower-cased models its datasets serve.

verdict property

The label a report prints for this provider: NOT USED or FAILED TO LOAD.

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, or that conflicts with another provider, is skipped and reported by fwl-io list and check_for, so one broken package cannot break data access for every other model.

Source code in src/fwl_io/manifest.py
635
636
637
638
639
640
641
642
643
644
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, or that conflicts
    with another provider, is skipped and reported by ``fwl-io list`` and
    ``check_for``, so one broken package cannot break data access for every
    other model.
    """
    return _discover_all().found

fetch_for(model, data_root=None, progress=False)

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
progress bool

Show a per-file download progress bar. When tqdm is not installed, or there is no sys.stderr to draw on, the bar is skipped and the fetch continues.

False
Source code in src/fwl_io/manifest.py
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
def fetch_for(
    model: str, data_root: str | Path | None = None, progress: bool = False
) -> 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.
    progress : bool
        Show a per-file download progress bar. When tqdm is not installed, or
        there is no ``sys.stderr`` to draw on, the bar is skipped and the fetch
        continues.
    """
    progress = _usable_progress(progress)
    model = model.lower()
    fetched: dict[str, list[Path]] = {}
    failures: dict[str, str] = {}
    discovery = _discover_all()
    providers = discovery.found
    provider_errors = discovery.errors_for(model)
    for datasets in providers.values():
        for ds in datasets:
            if model not in tuple(r.lower() for r in ds.required_by):
                continue
            try:
                fetched[ds.key] = _fetcher_of(ds, data_root, progress).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) not used:')
            by_message: dict[str, list[str]] = {}
            for name, error in sorted(provider_errors.items()):
                by_message.setdefault(error.message, []).append(name)
            report += [f'  {", ".join(names)}: {msg}' for msg, names in by_message.items()]
        raise RuntimeError('\n'.join(report))
    return fetched

fetcher_for_key(key, data_root=None, progress=False)

Return the fetcher of the dataset key declared in an installed manifest.

It is the fetcher :func:fetch_for uses, with the same mirrors and checks, so a code that cannot import fwl-io can fetch or locate one dataset through the CLI.

Parameters:

Name Type Description Default
key str

Dotted manifest key of the dataset, e.g. atmos_clim.refractive.agni_aerosols.

required
data_root str | Path | None

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

None
progress bool

Show a per-file download progress bar when one can be drawn.

False

Returns:

Type Description
Fetcher

Fetcher bound to the dataset's version directory.

Raises:

Type Description
LookupError

No usable installed manifest declares key.

Source code in src/fwl_io/manifest.py
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
def fetcher_for_key(key: str, data_root: str | Path | None = None, progress: bool = False):
    """Return the fetcher of the dataset ``key`` declared in an installed manifest.

    It is the fetcher :func:`fetch_for` uses, with the same mirrors and checks, so a code
    that cannot import fwl-io can fetch or locate one dataset through the CLI.

    Parameters
    ----------
    key : str
        Dotted manifest key of the dataset, e.g. ``atmos_clim.refractive.agni_aerosols``.
    data_root : str | Path | None
        Override for the data root; defaults to the resolved FWL_DATA tree.
    progress : bool
        Show a per-file download progress bar when one can be drawn.

    Returns
    -------
    fwl_io.Fetcher
        Fetcher bound to the dataset's version directory.

    Raises
    ------
    LookupError
        No usable installed manifest declares ``key``.
    """
    discovery = _discover_all()
    for datasets in discovery.found.values():
        for ds in datasets:
            if ds.key == key:
                return _fetcher_of(ds, data_root, _usable_progress(progress))
    n = len(discovery.errors)
    unused = f'; {n} manifest(s) not used, see fwl-io list' if n else ''
    raise LookupError(f'no installed manifest declares the dataset {key!r}{unused}')

load_manifest(path)

Load and validate all datasets declared in one manifest file.

Source code in src/fwl_io/manifest.py
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
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}'
            )
        files = table.get('files')
        if files is not None:
            files = _validate_files(key, files, extract)
        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,
                files=files,
            )
        )
    return datasets

shared_manifest_path()

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

Source code in src/fwl_io/manifest.py
465
466
467
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)