Skip to content

Client

High-level async client for interacting with packlayer.

Must be used as an async context manager to ensure the underlying HTTP session is properly opened and closed.

Parameters:

Name Type Description Default
minecraft_version MinecraftVersion | None

Optional Minecraft version filter.

None
concurrency int

Maximum simultaneous file downloads. Defaults to 8.

8
extra_resolvers list[ModpackResolver] | None

Additional resolvers registered before the built-ins, giving them higher priority. Useful for third-party providers or overriding built-in resolution behaviour. Each resolver must implement :class:~packlayer.interfaces.resolver.ModpackResolver and return True from can_handle only for sources it owns.

None
default_resolver ModpackResolver | None

Fallback resolver used when no registered resolver claims the source. If omitted and no resolver matches, :exc:~packlayer.NoResolverFound is raised.

None
config PacklayerConfig | None

Optional :class:~packlayer.domain.config.PacklayerConfig instance. Values from the config are used as defaults; explicit constructor arguments take precedence.

None
Example
    async with PacklayerClient(minecraft_version="1.20.1") as client:
        versions = await client.list_versions("mr:fabulously-optimized")
        modpack  = await client.resolve("mr:fabulously-optimized", modpack_version=versions[0].version_number)
        result   = await client.install(modpack, "./instance")
        print(f"{result.total} files installed ({len(result.downloads)} mods, {result.override_count} overrides)")
Plugin example
    async with PacklayerClient(extra_resolvers=[MyCurseForgeResolver()]) as client:
        modpack = await client.resolve("https://curseforge.com/minecraft/modpacks/...")
        await client.install(modpack, "./instance")
Source code in packlayer/client.py
class PacklayerClient:
    """
    High-level async client for interacting with packlayer.

    Must be used as an async context manager to ensure the underlying HTTP
    session is properly opened and closed.

    Parameters
    ----------
    minecraft_version:
        Optional Minecraft version filter.
    concurrency:
        Maximum simultaneous file downloads. Defaults to 8.
    extra_resolvers:
        Additional resolvers registered before the built-ins, giving them
        higher priority. Useful for third-party providers or overriding
        built-in resolution behaviour. Each resolver must implement
        :class:`~packlayer.interfaces.resolver.ModpackResolver` and return
        ``True`` from ``can_handle`` only for sources it owns.
    default_resolver:
        Fallback resolver used when no registered resolver claims the source.
        If omitted and no resolver matches, :exc:`~packlayer.NoResolverFound`
        is raised.
    config:
        Optional :class:`~packlayer.domain.config.PacklayerConfig` instance.
        Values from the config are used as defaults; explicit constructor
        arguments take precedence.

    Example
    -------
    ```python
        async with PacklayerClient(minecraft_version="1.20.1") as client:
            versions = await client.list_versions("mr:fabulously-optimized")
            modpack  = await client.resolve("mr:fabulously-optimized", modpack_version=versions[0].version_number)
            result   = await client.install(modpack, "./instance")
            print(f"{result.total} files installed ({len(result.downloads)} mods, {result.override_count} overrides)")
    ```

    Plugin example
    --------------
    ```python
        async with PacklayerClient(extra_resolvers=[MyCurseForgeResolver()]) as client:
            modpack = await client.resolve("https://curseforge.com/minecraft/modpacks/...")
            await client.install(modpack, "./instance")
    ```
    """

    def __init__(
        self,
        *,
        minecraft_version: MinecraftVersion | None = None,
        concurrency: int = 8,
        extra_resolvers: list[ModpackResolver] | None = None,
        default_resolver: ModpackResolver | None = None,
        config: PacklayerConfig | None = None,
    ) -> None:
        self._config = cfg = config or PacklayerConfig()
        self._minecraft_version = minecraft_version or cfg.minecraft_version
        self._concurrency = concurrency or cfg.concurrency
        self._extra_resolvers = extra_resolvers or []
        self._default_resolver = default_resolver

        self._http: PacklayerHTTP | None = None
        self._registry: ResolverRegistry | None = None

    async def __aenter__(self) -> PacklayerClient:
        self._http = PacklayerHTTP(retry=self._config.retry)
        await self._http.__aenter__()

        self._registry = ResolverRegistry()
        if self._default_resolver:
            self._registry.set_default_resolver(self._default_resolver)

        for r in self._extra_resolvers:
            self._registry.register(r)

        self._registry.register(FTBResolver(self._http, self._minecraft_version))
        self._registry.register(ModrinthResolver(self._http, self._minecraft_version))
        return self

    async def __aexit__(self, *_) -> None:
        if self._http:
            await self._http.__aexit__(None, None, None)
            self._http = None

    def resolvers(self) -> list[ModpackResolver]:
        """Return all registered resolvers in priority order."""
        return self._require_registry().resolvers()

    async def list_versions(self, source: str) -> list[ModpackVersion]:
        """
        Return all available versions of a modpack, newest-first.

        Dispatches to the appropriate registered resolver via
        :class:`~packlayer.providers.registry.ResolverRegistry`.
        Optionally filtered by the ``minecraft_version`` passed at
        construction time, if the resolver supports it.

        Parameters
        ----------
        source:
            A source string accepted by any registered resolver
            (e.g. ``"fabulously-optimized"``, ``"https://modrinth.com/modpack/..."``).

        Returns
        -------
        list[ModpackVersion]

        Raises
        ------
        NoResolverFound
            No registered resolver claimed the source.
        PacklayerError
            The resolver failed to fetch versions (slug not found, network error, etc.).
        """
        return await self._require_registry().pick(source).fetch_versions(source)

    async def resolve(
        self, source: str, *, modpack_version: str | None = None
    ) -> Modpack:
        """
        Resolve a modpack from ``source`` without downloading its files.

        Dispatches to the appropriate registered resolver via
        :class:`~packlayer.providers.registry.ResolverRegistry`. Returns a
        :class:`~packlayer.Modpack` containing metadata and the full file list,
        ready to be passed to :meth:`install`.

        Raises
        ------
        NoResolverFound
            No registered resolver claimed the source.
        PacklayerError
            Resolution failed (invalid file, slug not found, network error, etc.).
        """
        return await self.resolver_for(source).resolve(
            source, modpack_version=modpack_version
        )

    async def install(
        self,
        modpack: Modpack,
        dest: str | os.PathLike[str],
        *,
        on_start: Callable[[int], None] | None = None,
        on_progress: ProgressCallback | None = None,
        options: InstallOptions | None = None,
    ) -> InstallResult:
        """
        Install a modpack to ``dest``.

        Mods are placed in ``dest/mods/``. Override files (configs, scripts,
        resource packs, etc.) are written relative to ``dest/``, preserving
        the directory structure declared by the modpack.

        Parameters
        ----------
        modpack:
            A resolved [`Modpack`][packlayer.Modpack] from [`resolve`][resolve].
        dest:
            Instance root directory. Created if it does not exist. Mods go
            into ``dest/mods/``; overrides are written relative to ``dest/``.
        on_start:
            Optional callback invoked with the total file count (mods +
            overrides) before downloading starts.
        on_progress:
            Optional callback invoked after each file is installed (both mods
            and overrides). Sync and async callables are both accepted.
        options:
            Controls which files are downloaded. See [`InstallOptions`][packlayer.InstallOptions].

        Returns
        -------
        InstallResult

        Raises
        ------
        PacklayerError
            If download or hash verification fails.
        ValueError
            If ``dest`` exists but is not a directory.
        """
        dest = Path(dest).expanduser().resolve()
        if dest.exists() and not dest.is_dir():
            raise ValueError(f"destination must be a directory: {dest}")

        installer = InstallModpack(
            downloader=HttpDownloader(self._require_http()),
            on_start=on_start,
            on_progress=wrap_progress(on_progress) if on_progress else None,
            concurrency=self._concurrency,
            options=options,
        )
        return await installer.install(modpack, dest)

    def resolver_for(self, source: str) -> ModpackResolver:
        """Return the resolver that would handle ``source``."""
        return self._require_registry().pick(source)

    def _require_http(self) -> PacklayerHTTP:
        if self._http is None:
            raise RuntimeError(
                "PacklayerClient must be used as an async context manager:\n"
                "\n"
                "    async with PacklayerClient() as client:\n"
                "        ...\n"
            )
        return self._http

    def _require_registry(self) -> ResolverRegistry:
        if self._registry is None:
            raise RuntimeError(
                "PacklayerClient must be used as an async context manager:\n"
                "\n"
                "    async with PacklayerClient() as client:\n"
                "        ...\n"
            )
        return self._registry

install(modpack, dest, *, on_start=None, on_progress=None, options=None) async

Install a modpack to dest.

Mods are placed in dest/mods/. Override files (configs, scripts, resource packs, etc.) are written relative to dest/, preserving the directory structure declared by the modpack.

Parameters:

Name Type Description Default
modpack Modpack

A resolved Modpack from [resolve][].

required
dest str | PathLike[str]

Instance root directory. Created if it does not exist. Mods go into dest/mods/; overrides are written relative to dest/.

required
on_start Callable[[int], None] | None

Optional callback invoked with the total file count (mods + overrides) before downloading starts.

None
on_progress ProgressCallback | None

Optional callback invoked after each file is installed (both mods and overrides). Sync and async callables are both accepted.

None
options InstallOptions | None

Controls which files are downloaded. See [InstallOptions][packlayer.InstallOptions].

None

Returns:

Type Description
InstallResult

Raises:

Type Description
PacklayerError

If download or hash verification fails.

ValueError

If dest exists but is not a directory.

Source code in packlayer/client.py
async def install(
    self,
    modpack: Modpack,
    dest: str | os.PathLike[str],
    *,
    on_start: Callable[[int], None] | None = None,
    on_progress: ProgressCallback | None = None,
    options: InstallOptions | None = None,
) -> InstallResult:
    """
    Install a modpack to ``dest``.

    Mods are placed in ``dest/mods/``. Override files (configs, scripts,
    resource packs, etc.) are written relative to ``dest/``, preserving
    the directory structure declared by the modpack.

    Parameters
    ----------
    modpack:
        A resolved [`Modpack`][packlayer.Modpack] from [`resolve`][resolve].
    dest:
        Instance root directory. Created if it does not exist. Mods go
        into ``dest/mods/``; overrides are written relative to ``dest/``.
    on_start:
        Optional callback invoked with the total file count (mods +
        overrides) before downloading starts.
    on_progress:
        Optional callback invoked after each file is installed (both mods
        and overrides). Sync and async callables are both accepted.
    options:
        Controls which files are downloaded. See [`InstallOptions`][packlayer.InstallOptions].

    Returns
    -------
    InstallResult

    Raises
    ------
    PacklayerError
        If download or hash verification fails.
    ValueError
        If ``dest`` exists but is not a directory.
    """
    dest = Path(dest).expanduser().resolve()
    if dest.exists() and not dest.is_dir():
        raise ValueError(f"destination must be a directory: {dest}")

    installer = InstallModpack(
        downloader=HttpDownloader(self._require_http()),
        on_start=on_start,
        on_progress=wrap_progress(on_progress) if on_progress else None,
        concurrency=self._concurrency,
        options=options,
    )
    return await installer.install(modpack, dest)

list_versions(source) async

Return all available versions of a modpack, newest-first.

Dispatches to the appropriate registered resolver via :class:~packlayer.providers.registry.ResolverRegistry. Optionally filtered by the minecraft_version passed at construction time, if the resolver supports it.

Parameters:

Name Type Description Default
source str

A source string accepted by any registered resolver (e.g. "fabulously-optimized", "https://modrinth.com/modpack/...").

required

Returns:

Type Description
list[ModpackVersion]

Raises:

Type Description
NoResolverFound

No registered resolver claimed the source.

PacklayerError

The resolver failed to fetch versions (slug not found, network error, etc.).

Source code in packlayer/client.py
async def list_versions(self, source: str) -> list[ModpackVersion]:
    """
    Return all available versions of a modpack, newest-first.

    Dispatches to the appropriate registered resolver via
    :class:`~packlayer.providers.registry.ResolverRegistry`.
    Optionally filtered by the ``minecraft_version`` passed at
    construction time, if the resolver supports it.

    Parameters
    ----------
    source:
        A source string accepted by any registered resolver
        (e.g. ``"fabulously-optimized"``, ``"https://modrinth.com/modpack/..."``).

    Returns
    -------
    list[ModpackVersion]

    Raises
    ------
    NoResolverFound
        No registered resolver claimed the source.
    PacklayerError
        The resolver failed to fetch versions (slug not found, network error, etc.).
    """
    return await self._require_registry().pick(source).fetch_versions(source)

resolve(source, *, modpack_version=None) async

Resolve a modpack from source without downloading its files.

Dispatches to the appropriate registered resolver via :class:~packlayer.providers.registry.ResolverRegistry. Returns a :class:~packlayer.Modpack containing metadata and the full file list, ready to be passed to :meth:install.

Raises:

Type Description
NoResolverFound

No registered resolver claimed the source.

PacklayerError

Resolution failed (invalid file, slug not found, network error, etc.).

Source code in packlayer/client.py
async def resolve(
    self, source: str, *, modpack_version: str | None = None
) -> Modpack:
    """
    Resolve a modpack from ``source`` without downloading its files.

    Dispatches to the appropriate registered resolver via
    :class:`~packlayer.providers.registry.ResolverRegistry`. Returns a
    :class:`~packlayer.Modpack` containing metadata and the full file list,
    ready to be passed to :meth:`install`.

    Raises
    ------
    NoResolverFound
        No registered resolver claimed the source.
    PacklayerError
        Resolution failed (invalid file, slug not found, network error, etc.).
    """
    return await self.resolver_for(source).resolve(
        source, modpack_version=modpack_version
    )

resolver_for(source)

Return the resolver that would handle source.

Source code in packlayer/client.py
def resolver_for(self, source: str) -> ModpackResolver:
    """Return the resolver that would handle ``source``."""
    return self._require_registry().pick(source)

resolvers()

Return all registered resolvers in priority order.

Source code in packlayer/client.py
def resolvers(self) -> list[ModpackResolver]:
    """Return all registered resolvers in priority order."""
    return self._require_registry().resolvers()

Install a Minecraft modpack in a single call.

Convenience wrapper around :class:PacklayerClient. For multiple operations, fine-grained control, or custom resolvers, use the client directly.

Parameters:

Name Type Description Default
source str

Any source string accepted by a registered resolver — local path, direct URL, or provider-prefixed ID (e.g. "mr:fabulously-optimized", "ftb:79").

required
dest str | PathLike[str]

Instance root directory. Mods go into dest/mods/; overrides are written relative to dest/.

required
minecraft_version str | None

Optional Minecraft version filter, passed through to resolvers that support it.

None
modpack_version str | None

Pin a specific modpack version (e.g. "6.0.1"). If omitted, the latest compatible version is used.

None
concurrency int

Maximum simultaneous downloads. Defaults to 8.

8
on_start Callable[[int], None] | None

Optional callback invoked with the total file count (mods + overrides) before downloading starts.

None
on_progress ProgressCallback | None

Optional callback (sync or async) invoked after each installed file (both mods and overrides).

None
options InstallOptions | None

Controls which files are downloaded. See :class:~packlayer.InstallOptions.

None
extra_resolvers list[ModpackResolver] | None

Additional resolvers registered before the built-ins.

None
default_resolver ModpackResolver | None

Fallback resolver when no registered resolver claims the source.

None

Returns:

Type Description
InstallResult

Raises:

Type Description
NoResolverFound

No registered resolver claimed the source.

PacklayerError

If resolution, download, or verification fails.

Examples:

Basic::

asyncio.run(install_modpack("mr:fabulously-optimized", "./instance"))

With async progress::

async def on_file():
    await ws.send("progress")

asyncio.run(
    install_modpack("mr:pack.mrpack", "./instance", on_progress=on_file)
)
Source code in packlayer/client.py
async def install_modpack(
    source: str,
    dest: str | os.PathLike[str],
    *,
    minecraft_version: str | None = None,
    modpack_version: str | None = None,
    concurrency: int = 8,
    on_start: Callable[[int], None] | None = None,
    on_progress: ProgressCallback | None = None,
    options: InstallOptions | None = None,
    extra_resolvers: list[ModpackResolver] | None = None,
    default_resolver: ModpackResolver | None = None,
) -> InstallResult:
    """
    Install a Minecraft modpack in a single call.

    Convenience wrapper around :class:`PacklayerClient`. For multiple
    operations, fine-grained control, or custom resolvers, use the client
    directly.

    Parameters
    ----------
    source:
        Any source string accepted by a registered resolver — local path,
        direct URL, or provider-prefixed ID (e.g. ``"mr:fabulously-optimized"``,
        ``"ftb:79"``).
    dest:
        Instance root directory. Mods go into ``dest/mods/``; overrides are
        written relative to ``dest/``.
    minecraft_version:
        Optional Minecraft version filter, passed through to resolvers
        that support it.
    modpack_version:
        Pin a specific modpack version (e.g. ``"6.0.1"``). If omitted,
        the latest compatible version is used.
    concurrency:
        Maximum simultaneous downloads. Defaults to 8.
    on_start:
        Optional callback invoked with the total file count (mods + overrides)
        before downloading starts.
    on_progress:
        Optional callback (sync or async) invoked after each installed file
        (both mods and overrides).
    options:
        Controls which files are downloaded. See :class:`~packlayer.InstallOptions`.
    extra_resolvers:
        Additional resolvers registered before the built-ins.
    default_resolver:
        Fallback resolver when no registered resolver claims the source.

    Returns
    -------
    InstallResult

    Raises
    ------
    NoResolverFound
        No registered resolver claimed the source.
    PacklayerError
        If resolution, download, or verification fails.

    Examples
    --------
    Basic::

        asyncio.run(install_modpack("mr:fabulously-optimized", "./instance"))

    With async progress::

        async def on_file():
            await ws.send("progress")

        asyncio.run(
            install_modpack("mr:pack.mrpack", "./instance", on_progress=on_file)
        )
    """
    async with PacklayerClient(
        minecraft_version=minecraft_version,
        concurrency=concurrency,
        extra_resolvers=extra_resolvers,
        default_resolver=default_resolver,
    ) as client:
        modpack = await client.resolve(source, modpack_version=modpack_version)
        return await client.install(
            modpack,
            dest,
            on_start=on_start,
            on_progress=on_progress,
            options=options,
        )