Async minecraft modpack resolver and downloader
Project description
packlayer
Resolves and installs Minecraft modpacks from Modrinth slugs, FTB IDs, direct URLs, or local .mrpack files.
One call. No launcher required.
What it does
You give it a modpack source — a Modrinth slug, an FTB pack ID, a direct .mrpack URL, or a local file — and it resolves the manifest, downloads all mod files concurrently, verifies their integrity, and drops them into a folder. There's a CLI for one-off use and a full async Python API for integration.
The resolver is provider-agnostic by design. Built-in support covers Modrinth and FTB. Additional providers can be plugged in without touching the library.
Installation
pip install packlayer
Requirements: Python 3.14+
Usage
CLI
packlayer install mr:fabulously-optimized
packlayer install https://modrinth.com/modpack/fabulously-optimized
packlayer install ftb:79
packlayer install ./mypack.mrpack
packlayer install mr:fabulously-optimized --minecraft 1.20.1 --dest ./dest
All options
| Flag | Description |
|---|---|
--dest <path> |
Output directory (default: ./dest) |
--version <version> |
Pin a specific modpack version (e.g. 6.0.1) |
--minecraft <version> |
Filter by Minecraft version (e.g. 1.20.1) |
--side <client|server|both> |
Which side to install for (default: client) |
--no-optional |
Skip optional mods |
-v, --verbose |
Enable debug logging |
Python API
One-shot
import asyncio
from packlayer import install_modpack
asyncio.run(install_modpack("mr:fabulously-optimized", "./mods"))
Client
import asyncio
from packlayer import PacklayerClient
async def main():
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)
results = await client.install(modpack, "./mods")
print(f"{results.total} files installed")
asyncio.run(main())
Progress tracking
from packlayer import PacklayerClient
async def main():
async with PacklayerClient() as client:
modpack = await client.resolve("mr:fabulously-optimized")
def on_start(total: int) -> None:
print(f"downloading {total} files")
def on_progress() -> None:
print(".", end="", flush=True)
await client.install(
modpack, "./mods",
on_start=on_start,
on_progress=on_progress,
)
on_progress is called once per installed file (mods and overrides). Both sync and async callables are accepted.
Install options
from packlayer import PacklayerClient, InstallOptions
async def main():
async with PacklayerClient() as client:
modpack = await client.resolve("ftb:79")
await client.install(
modpack, "./mods",
options=InstallOptions(
side="server",
include_optional=False,
),
)
Reference
install_modpack
async def install_modpack(
source: str,
dest: str | 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
| Parameter | Description |
|---|---|
source |
Local path, direct URL, mr:<slug>, Modrinth project URL, or ftb:<id> |
dest |
Destination directory. Created if it does not exist |
minecraft_version |
Filter versions by Minecraft version (e.g. "1.20.1") |
modpack_version |
Pin a specific modpack version (e.g. "6.0.1"). Uses latest if omitted |
concurrency |
Max simultaneous downloads. Default: 8 |
on_start |
Callback invoked with the total file count before downloading starts |
on_progress |
Callback invoked after each installed file (sync or async, no arguments) |
options |
Controls which files are installed. See InstallOptions |
extra_resolvers |
Additional resolvers registered before built-ins |
default_resolver |
Fallback resolver when no registered resolver claims the source |
PacklayerClient
class PacklayerClient:
def __init__(
self,
*,
minecraft_version: str | None = None,
concurrency: int = 8,
extra_resolvers: list[ModpackResolver] | None = None,
default_resolver: ModpackResolver | None = None,
config: PacklayerConfig | None = None,
) -> None
Must be used as an async context manager.
| Method | Description |
|---|---|
resolve(source, *, modpack_version) |
Resolve a modpack without downloading files |
install(modpack, dest, *, on_start, on_progress, options) |
Install a resolved modpack to disk |
list_versions(source) |
Return available versions, newest-first |
resolver_for(source) |
Return the resolver that would handle source |
resolvers() |
Return all registered resolvers in priority order |
Models
Modpack
| Field | Type | Description |
|---|---|---|
name |
str |
Display name |
version |
str |
Version string |
minecraft_version |
str |
Target Minecraft version |
files |
tuple[ModFile, ...] |
Mod files to be downloaded |
overrides |
tuple[Override, ...] |
Non-mod files to be written into the instance directory |
ModFile
| Field | Type | Description |
|---|---|---|
url |
str |
Download URL |
filename |
str |
Bare filename |
size |
int |
Expected file size in bytes |
hash |
str | None |
Hex digest, verified post-download if provided |
hash_type |
"sha512" | "sha1" | None |
Algorithm used for hash |
optional |
bool |
Whether the file is optional |
side |
"client" | "server" | "both" |
Which side this file targets |
Override
| Field | Type | Description |
|---|---|---|
path |
str |
Destination path relative to the instance root (e.g. "config/sodium.json") |
side |
"client" | "server" | "both" |
Which side this override applies to |
data |
bytes | None |
Raw file contents bundled inline (e.g. from .mrpack zips). Mutually exclusive with url |
url |
str | None |
Remote URL to fetch the file from (e.g. FTB overrides). Mutually exclusive with data |
ModpackVersion
| Field | Type | Description |
|---|---|---|
id |
str |
Provider-assigned version ID |
version_number |
str |
Human-readable version string |
name |
str |
Release display name |
loaders |
tuple[str, ...] |
Supported mod loaders |
game_versions |
tuple[str, ...] |
Compatible Minecraft versions |
date_published |
str |
ISO 8601 publish timestamp |
InstallOptions
| Field | Type | Default | Description |
|---|---|---|---|
include_optional |
bool |
True |
If False, optional files are skipped |
side |
"client" | "server" | "both" |
"client" |
Files incompatible with this side are skipped |
Exceptions
All exceptions inherit from PacklayerError.
| Exception | Description |
|---|---|
LocalFileNotFound |
Local path does not exist |
InvalidMrpack |
File is not a valid .mrpack archive |
SlugNotFound |
No project matches the given slug or ID |
NoVersionFound |
Project exists but has no compatible version |
NoResolverFound |
No registered resolver claimed the source |
HashMismatch |
Hash digest mismatch after download |
NetworkError |
Network failure |
Supported providers
| Provider | Source format | Auth required |
|---|---|---|
| Modrinth | mr:<slug>, Modrinth project URL, direct .mrpack URL, local .mrpack |
No |
| FTB | ftb:<id>, feed-the-beast.com URL |
No |
Plugin system
packlayer dispatches resolution to a registry of ModpackResolver instances. extra_resolvers are registered before built-ins, giving them higher priority.
Implementing a resolver
from packlayer.interfaces.resolver import ModpackResolver
from packlayer.domain.models import Modpack, ModpackVersion
class MyResolver(ModpackResolver):
def can_handle(self, source: str) -> bool:
return "myprovider.com" in source
async def resolve(self, source: str, *, modpack_version: str | None = None) -> Modpack:
...
async def fetch_versions(self, source: str) -> list[ModpackVersion]:
...
can_handle must be exclusive — return True only for sources this resolver definitively owns.
Registering
async with PacklayerClient(extra_resolvers=[MyResolver()]) as client:
modpack = await client.resolve("https://myprovider.com/modpacks/mypack")
await client.install(modpack, "./mods")
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file packlayer-0.1.0.tar.gz.
File metadata
- Download URL: packlayer-0.1.0.tar.gz
- Upload date:
- Size: 76.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a178c7058accefbac5718c01f26daf6d0c2c21974eaa54a00650767222792c1
|
|
| MD5 |
f741bbb323079a9a11a5cb445682f231
|
|
| BLAKE2b-256 |
2336fbf201f54a850e36236667cfc6a0acbd2cf6f1d933140db313ac4edd6e33
|
File details
Details for the file packlayer-0.1.0-py3-none-any.whl.
File metadata
- Download URL: packlayer-0.1.0-py3-none-any.whl
- Upload date:
- Size: 31.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4560fcea36cf939dd6233cbf5355ff7e45e2d19de117d064f0b68c8521e1bc3b
|
|
| MD5 |
0f6fdc04b3be8ccf9d91297789b9b7a1
|
|
| BLAKE2b-256 |
9321cb885187997396b7554618c027b1b37063218fa9d084e225fa399cdd059e
|