Skip to content

discovery.manager

napt.discovery.manager

Discovery pipeline orchestration.

This module owns the top-level discover_recipe entry point used by napt discover. It loads the merged configuration, picks a flow based on the recipe's discovery.strategy value, persists the discovery cache for the next run, records the release as a pending publication candidate in deployment state, and returns the public DiscoverResult.

Two Flows

Two flows feed into the same orchestration:

  • Version-first (api_github, api_json, web_scrape and any other registered DiscoveryStrategy): the strategy returns a RemoteVersion, and resolve_with_cache decides whether to skip the download (version unchanged + file present) or fetch fresh.
  • url_download (handled by run_url_download): downloads the file with HTTP conditional headers (ETag / Last-Modified) and extracts the version from the file's metadata. Not a registered strategy because it cannot determine the version without the file.

Both flows return a StrategyResult, which this module unwraps into state-cache fields and the public result.

discover_recipe

discover_recipe(
    recipe_path: Path,
    output_dir: Path | None = None,
    cache_file: Path | None = None,
    state_dir: Path | None = None,
    stateless: bool = False,
) -> DiscoverResult

Discovers the latest version of an app and resolves its installer.

Loads the recipe configuration, dispatches to the appropriate discovery flow (version-first registered strategy or url_download), persists the result to the discovery cache, records the release as the pending publication candidate in deployment state when it differs from the published version, and returns the public discovery result.

Parameters:

Name Type Description Default
recipe_path Path

Path to the recipe YAML file. Must exist and be readable.

required
output_dir Path | None

Directory to download the installer into. When omitted, falls back to directories.discover from the merged configuration. Created if it does not exist.

None
cache_file Path | None

Path to the discovery cache file. When omitted, falls back to <directories.cache>/discovery.json from the merged configuration.

None
state_dir Path | None

Directory holding per-app deployment state files. When omitted, falls back to <directories.state>/deployment from the merged configuration.

None
stateless bool

When True, skips loading and saving the discovery cache and deployment state entirely. Forces every run to behave as if no prior cache existed.

False

Returns:

Type Description
DiscoverResult

Public discovery result containing the resolved version,

DiscoverResult

file path, and SHA-256 hash.

Raises:

Type Description
ConfigError

On missing or invalid configuration, including an unknown discovery.strategy value.

NetworkError

On download or version-extraction failures from either flow.

StateError

On a corrupted deployment state file.

Source code in napt/discovery/manager.py
def discover_recipe(
    recipe_path: Path,
    output_dir: Path | None = None,
    cache_file: Path | None = None,
    state_dir: Path | None = None,
    stateless: bool = False,
) -> DiscoverResult:
    """Discovers the latest version of an app and resolves its installer.

    Loads the recipe configuration, dispatches to the appropriate
    discovery flow (version-first registered strategy or ``url_download``),
    persists the result to the discovery cache, records the release as the
    pending publication candidate in deployment state when it differs from
    the published version, and returns the public discovery result.

    Args:
        recipe_path: Path to the recipe YAML file. Must exist and be
            readable.
        output_dir: Directory to download the installer into. When
            omitted, falls back to ``directories.discover`` from the
            merged configuration. Created if it does not exist.
        cache_file: Path to the discovery cache file. When omitted,
            falls back to ``<directories.cache>/discovery.json`` from
            the merged configuration.
        state_dir: Directory holding per-app deployment state files.
            When omitted, falls back to ``<directories.state>/deployment``
            from the merged configuration.
        stateless: When True, skips loading and saving the discovery
            cache and deployment state entirely. Forces every run to
            behave as if no prior cache existed.

    Returns:
        Public discovery result containing the resolved version,
        file path, and SHA-256 hash.

    Raises:
        ConfigError: On missing or invalid configuration, including
            an unknown ``discovery.strategy`` value.
        NetworkError: On download or version-extraction failures from
            either flow.
        StateError: On a corrupted deployment state file.

    """
    logger = get_global_logger()

    logger.step(1, 4, "Loading configuration...")
    config = load_effective_config(recipe_path)
    if output_dir is None:
        output_dir = Path(config["directories"]["discover"])
    if cache_file is None:
        cache_file = cache_file_path(config)
    if state_dir is None:
        state_dir = Path(config["directories"]["state"]) / "deployment"

    cache_data = _load_cache(cache_file, stateless, logger)

    app_name = config["name"]
    app_id = config["id"]

    discovery = config.get("discovery", {})
    strategy_name = discovery.get("strategy")
    if not strategy_name:
        raise ConfigError(f"No 'discovery.strategy' defined for app: {app_name}")

    cache = _get_cache_for_app(cache_data, app_id, logger)

    logger.step(2, 4, "Discovering version...")
    if strategy_name == "url_download":
        logger.step(3, 4, "Fetching installer...")
        result = run_url_download(config, output_dir, cache=cache)
    else:
        strategy = get_strategy(strategy_name)
        info = strategy.discover(config)
        logger.info("DISCOVERY", f"Version discovered: {info.version}")
        logger.step(3, 4, "Resolving installer...")
        result = resolve_with_cache(info, config, output_dir, cache)

    logger.step(4, 4, "Updating state...")
    if not stateless and app_id:
        if cache_data is not None:
            _save_app_cache(
                cache_data, cache_file, app_id, strategy_name, result, logger
            )
        _record_pending_release(state_dir, app_id, app_name, result, logger)

    return DiscoverResult(
        app_name=app_name,
        app_id=app_id,
        strategy=strategy_name,
        version=result.version,
        version_source=result.version_source,
        file_path=result.file_path,
        sha256=result.sha256,
        status="success",
    )