Skip to content

cli

meds-extract-download — CLI entry point for the download layer.

Reads a MESSY spec’s sources: block and runs each resolved source’s :meth:~MEDS_extract.download.source.Source.download_all in sequence. Sources are processed one at a time; per-file fetches within a source share one :class:~concurrent.futures.ThreadPoolExecutor sized to the user’s concurrency= argument, so the per-file transport bound is a global cap.

Written as a Hydra entry point so override syntax matches the rest of the pipeline. To re-run against a local mirror instead of the original remote, edit the spec’s sources: block (or keep a second bucket — e.g. a mirror: bucket with a type: fsspec entry — and select it via key=mirror); the source definitions themselves live in the spec file, not in Hydra’s config.

The config schema is a hydra_registered_dataclass (from MEDS-transforms): it registers with Hydra’s ConfigStore and types as a dataclass, so cfg.spec / cfg.do_overwrite / etc are typed attributes.

DownloadConfig

Typed config for meds-extract-download.

Fields

spec: Path or pkg:// reference to the MESSY spec YAML with a sources: block. output_dir: Destination directory under which fetched files land. key: Which sources: bucket to pull. "common" is always appended. Must name a bucket that actually exists in the spec (guards against typos silently downloading nothing). concurrency: Max parallel transport streams across all sources (one shared pool). continue_on_error: If True, per-file failures don’t sink the run; every source is still attempted and the process exits non-zero at the end if anything failed. If False (default), the first failing source stops the whole run. do_overwrite: If True, re-fetch every file even if the local copy matches.

Source code in MEDS_extract/download/cli.py
@hydra_registered_dataclass(group=None, name="download_defaults")
class DownloadConfig:
    """Typed config for ``meds-extract-download``.

    Fields:
        spec: Path or ``pkg://`` reference to the MESSY spec YAML with a
            ``sources:`` block.
        output_dir: Destination directory under which fetched files land.
        key: Which ``sources:`` bucket to pull. ``"common"`` is always appended.
            Must name a bucket that actually exists in the spec (guards against
            typos silently downloading nothing).
        concurrency: Max parallel transport streams across all sources (one shared pool).
        continue_on_error: If ``True``, per-file failures don't sink the run; every
            source is still attempted and the process exits non-zero at the end if
            anything failed. If ``False`` (default), the first failing source stops
            the whole run.
        do_overwrite: If ``True``, re-fetch every file even if the local copy matches.
    """

    spec: str = MISSING
    output_dir: str = MISSING
    key: str = "dataset"
    concurrency: int = 4
    continue_on_error: bool = False
    do_overwrite: bool = False

_hydra_main(cfg)

Hydra task function for meds-extract-download; see :func:main.

Required args (Hydra dotlist syntax):

  • spec=/path/to/event_configs.yaml — the MESSY spec with a sources: block (a pkg:// reference also works).
  • output_dir=/path/to/output — where the fetched files land

Optional args:

  • key=dataset (default) / key=demo — which sources: bucket to pull. common is always appended. When the spec declares sources buckets, a key naming none of them is an error (not a silent no-op); a spec with no sources: block at all is a legitimately download-free ETL and warns + exits 0 regardless of key.
  • concurrency=4 (default) — max parallel transport streams across all sources. One :class:~concurrent.futures.ThreadPoolExecutor is shared by every source’s download_all call so the bound applies globally rather than per-source.
  • continue_on_error=False (default) — if True, per-file failures don’t sink the run and every source is attempted; if False, the first failing source stops the whole run.
  • do_overwrite=False (default) — if True, re-fetch every file even if the local copy matches the manifest.

Exits 0 on full success and 1 on any failure, via an explicit :func:sys.exit — Hydra discards the task function’s return value, so a plain return 1 would not reach the process exit code.

Source code in MEDS_extract/download/cli.py
@hydra.main(version_base=None, config_path=".", config_name="_cli")
def _hydra_main(cfg: DictConfig) -> None:
    """Hydra task function for ``meds-extract-download``; see :func:`main`.

    Required args (Hydra dotlist syntax):

    - ``spec=/path/to/event_configs.yaml`` — the MESSY spec with a ``sources:`` block
      (a ``pkg://`` reference also works).
    - ``output_dir=/path/to/output`` — where the fetched files land

    Optional args:

    - ``key=dataset`` (default) / ``key=demo`` — which ``sources:`` bucket to pull.
      ``common`` is always appended. When the spec declares sources buckets, a
      ``key`` naming none of them is an error (not a silent no-op); a spec with no
      ``sources:`` block at all is a legitimately download-free ETL and warns +
      exits 0 regardless of ``key``.
    - ``concurrency=4`` (default) — max parallel transport streams across all sources.
      One :class:`~concurrent.futures.ThreadPoolExecutor` is shared by every source's
      ``download_all`` call so the bound applies globally rather than per-source.
    - ``continue_on_error=False`` (default) — if True, per-file failures don't sink the
      run and every source is attempted; if False, the first failing source stops the
      whole run.
    - ``do_overwrite=False`` (default) — if True, re-fetch every file even if the local
      copy matches the manifest.

    Exits ``0`` on full success and ``1`` on any failure, via an explicit
    :func:`sys.exit` — Hydra discards the task function's *return* value, so a
    plain ``return 1`` would not reach the process exit code.
    """
    # One load of the one config object, then everything comes off it. Errors —
    # spec resolution, a URL-shaped output_dir, document validation, bad bucket key,
    # malformed source entries, unresolvable interpolations in the selected bucket —
    # are user config mistakes: log them and exit 1, mirroring the manifest-validation
    # handling below, rather than dumping a raw traceback through Hydra.
    try:
        output_dir = user_local_path(str(cfg.output_dir), field="output_dir")
        messy = MessyConfig.load(str(cfg.spec))
        sources = messy.selected_sources(key=cfg.key)
    except (TypeError, ValueError, FileNotFoundError) as e:
        logger.error(f"Could not construct sources from the spec: {e}")
        sys.exit(1)

    if not sources:
        logger.warning(f"No sources resolved for key={cfg.key!r} in {messy.source_fp}. Nothing to do.")
        return

    # Teardown notes:
    #
    # - ``shutdown(wait=False, cancel_futures=True)`` cancels *queued* futures
    #   immediately. Worker threads are NOT daemon threads (since Python 3.9,
    #   bpo-39812), so the interpreter joins any still-running workers at exit —
    #   in-flight transfers finish (or die when their transport is torn down)
    #   before the process can exit.
    # - The ExitStack closes sources LIFO *before* the pool-shutdown callback runs,
    #   so on any exit path each owned ``httpx.Client`` is closed while workers may
    #   still be streaming — those in-flight HTTP transfers fail fast rather than
    #   draining, which is what keeps Ctrl+C reasonably prompt for HTTP sources.
    #   Fsspec copies have no equivalent abort path and run to completion.
    with ExitStack() as stack:
        pool = ThreadPoolExecutor(max_workers=cfg.concurrency)
        stack.callback(pool.shutdown, wait=False, cancel_futures=True)
        for source in sources:
            stack.enter_context(source)

        # Materializes every source's manifest up-front (cached for the fetch loop
        # below) and fails before any fetch into output_dir if a manifest row is
        # malformed or two sources would write the same file. (Manifest listing
        # itself may do network I/O — e.g. PhysioNet's SHA256SUMS.txt GET, fsspec
        # source-side hashing.)
        try:
            validate_unique_destinations(sources)
        except ValueError as e:
            logger.error(f"Source manifests failed validation: {e}")
            sys.exit(1)

        all_ok = True
        for source in sources:
            try:
                source.download_all(
                    output_dir,
                    pool=pool,
                    continue_on_error=cfg.continue_on_error,
                    do_overwrite=cfg.do_overwrite,
                )
            except Exception:
                logger.exception(f"download_all failed for {type(source).__name__}")
                all_ok = False
                if not cfg.continue_on_error:
                    # Fail fast applies across sources too: don't start source N+1
                    # after source N has already sunk the run.
                    break
        if not all_ok:
            sys.exit(1)

main()

Console-script entry point for meds-extract-download.

Validates the required dotlist args before Hydra owns the process: the Hydra run dir is anchored at ${output_dir}/..., so without this check a bare invocation would die inside interpolation resolution instead of printing usage — and a failed invocation must create no directories anywhere.

Source code in MEDS_extract/download/cli.py
def main() -> None:
    """Console-script entry point for ``meds-extract-download``.

    Validates the required dotlist args before Hydra owns the process: the Hydra run
    dir is anchored at ``${output_dir}/...``, so without this check a bare invocation
    would die inside interpolation resolution instead of printing usage — and a failed
    invocation must create no directories anywhere.
    """
    require_dotlist_args(
        "meds-extract-download",
        {"spec": "<pkg://...|/path|./path>", "output_dir": "<dir>"},
        local_only=("output_dir",),
    )
    _hydra_main()