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 state for the next run, 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,
    state_file: Path | None = Path("state/versions.json"),
    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 state cache, 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
state_file Path | None

Path to the state file used for caching. Defaults to state/versions.json. Set to None to disable persistence (run still uses an in-memory cache).

Path('state/versions.json')
stateless bool

When True, skips loading and saving 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.

Source code in napt/discovery/manager.py
def discover_recipe(
    recipe_path: Path,
    output_dir: Path | None = None,
    state_file: Path | None = Path("state/versions.json"),
    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 state cache, 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.
        state_file: Path to the state file used for caching. Defaults
            to ``state/versions.json``. Set to ``None`` to disable
            persistence (run still uses an in-memory cache).
        stateless: When True, skips loading and saving 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.

    """
    logger = get_global_logger()

    state = _load_state(state_file, stateless, logger)

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

    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(state, 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 state is not None and app_id and state_file:
        _save_app_state(state, state_file, app_id, strategy_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",
    )