Metadata-Version: 2.4
Name: modmux
Version: 0.6.0
Summary: Single interface for several game mod portals
Author: APasz
License: MIT
License-File: LICENSE
Requires-Python: >=3.13
Requires-Dist: aiolimiter<2,>=1.2
Requires-Dist: httpx<1,>=0.28
Requires-Dist: pydantic<3,>=2
Description-Content-Type: text/markdown

# ModMux

[![PyPI](https://img.shields.io/pypi/v/modmux.svg?style=for-the-badge)](https://pypi.org/project/modmux/)
[![Python](https://img.shields.io/badge/python-3.13%2B-blue.svg?style=for-the-badge)](https://pypi.org/project/modmux/)
[![License](https://img.shields.io/github/license/APasz/ModMux.svg?style=for-the-badge)](LICENSE)
[![CI](https://img.shields.io/github/actions/workflow/status/APasz/ModMux/ci.yml?style=for-the-badge)](https://github.com/APasz/ModMux/actions/workflows/ci.yml)

Unified async client for multiple game mod platforms. ModMux normalises provider responses into shared Pydantic models and ships a small CLI for quick lookups.

## Features
- Async HTTP client with rate limiting and retry handling
- Normalised `Mod` metadata model across providers
- Bulk `get_mods(...)` support with native batching where available
- Pluggable provider registry
- URL parsing helpers for provider links
- CLI for fetching one or more mods and printing JSON

## Supported providers
- Modrinth
- CurseForge
- Nexus Mods
- Mod.io
- Steam Workshop
- Factorio Mod Portal (Wube)
- transportfever.net

## Requirements
- Python 3.13+
- Dependencies: `pydantic`, `httpx`, `aiolimiter`

## Install
```bash
python -m pip install modmux
```

### Credentials and environment variables
- `--token` accepts API tokens/keys.
- `--user` supplies a user id for providers that need user-scoped base URLs (mod.io).
- Environment variables:
  - `MODMUX_TOKEN` (fallback for all providers)
  - `MODMUX_<PROVIDER>_TOKEN` (provider-specific, e.g. `MODMUX_MODRINTH_TOKEN`)
  - `MODMUX_<PROVIDER>_USER` (provider-specific user id, e.g. `MODMUX_MODIO_USER`)

## Library usage
Two supported patterns are available: an async context manager or a manually
closed client.

```python
import asyncio

from modmux import ModID, ModrinthCreds, Muxer, Provider

mod_id = ModID(provider=Provider.MODRINTH, id="fabric-api")


async def run() -> None:
    async with Muxer(creds=[ModrinthCreds(api_key="api-key")]) as mux:
        mod = await mux.get_mod(Provider.MODRINTH, mod_id)
    print(mod.name)


asyncio.run(run())
```

Use the same context manager pattern as above; you can supply multiple provider credentials in one go:

```python
from modmux import ModioCreds, Muxer, NexusCreds, SteamCreds

mux = Muxer(
    creds=[
        ModioCreds(api_key="api-key", user_id="user-id"),
        NexusCreds(token="nexus-token"),
        SteamCreds(api_key="steam-key"),
    ]
)
```

```python
import asyncio

from modmux import ModID, Muxer, Provider

mod_id = ModID(provider=Provider.MODRINTH, id="fabric-api")


async def run() -> None:
    mux = Muxer()
    try:
        mod = await mux.get_mod(Provider.MODRINTH, mod_id)
    finally:
        await mux.aclose()
    print(mod.name)


asyncio.run(run())
```

The `modmux_client(...)` helper remains available as a convenience wrapper
around `Muxer` for existing code.

You can also pass raw credential dicts when you do not want to import the
credential models directly:

```python
from modmux import ModID, Muxer, Provider

mod_id = ModID(provider=Provider.MODRINTH, id="fabric-api")

async with Muxer(creds={Provider.MODRINTH: {"token": "token"}}) as mux:
    mod = await mux.get_mod(Provider.MODRINTH, mod_id)
```

You can also fetch multiple mods in one call:

```python
from modmux import ModID, Muxer, Provider

mod_ids = [
    ModID(provider=Provider.STEAM, id="123"),
    ModID(provider=Provider.STEAM, id="456"),
]

async with Muxer() as mux:
    mods = await mux.get_mods(Provider.STEAM, mod_ids)
```

## URL parsing
Parse provider URLs into `ModID` instances, or fetch directly from a URL.

```python
import asyncio

from modmux import Muxer, parse_url


async def run_mod_id() -> None:
    async with Muxer() as mux:
        mod = await mux.get_mod_from_url("https://modrinth.com/mod/fabric-api")
    print(mod.name)

async def run_from_url() -> None:
    mod_id = parse_url("https://modrinth.com/mod/fabric-api")
    if mod_id:
        async with Muxer() as mux:
            mod = await mux.get_mod(mod_id.provider, mod_id)
        print(mod.name)

asyncio.run(run())
```

Some providers require extra context (game ids or credentials); use
`get_mod_from_url(..., game="...")` when the URL cannot supply it.

## CLI usage
The CLI expects a provider name and one or more provider-specific mod ids. Provider names are case-insensitive.

```bash
modmux MODRINTH fabric-api --pretty
modmux CURSEFORGE 238222 --pretty
modmux STEAM 123 456 --pretty
modmux NEXUSMODS 12345 --game transportfever2
modmux MODIO some-mod --game 4321 --user 12345 --token <api-key>
modmux TRANSPORTFEVERNET 7867 --pretty
modmux TRANSPORTFEVERNET urbanist_classic_station_1 --game tpf1 --pretty
modmux --from-urls urls.txt --pretty
```

Or without installing a script:

```bash
python -m modmux MODRINTH fabric-api --pretty
```

Output is a JSON serialisation of a single `Mod` when one id is provided, or a JSON array of `Mod` objects when multiple ids are provided.

When using `--from-urls`, provide one mod URL per line. Blank lines and lines starting with `#` are ignored. URLs are grouped by provider internally so the CLI can reuse native bulk lookups where available.

## Provider notes
- CurseForge: slug lookups require `ModID.game` (numeric game id or game slug from a CurseForge URL).
- Nexus Mods: requires `ModID.game` (game domain, e.g. `skyrim`).
- mod.io: requires `ModID.game` plus a user id (use `--user` or `MODMUX_MODIO_USER`).
- Steam: uses a Workshop published file id; `ModID.game` is optional.
- transportfever.net: uses the TPF2 repository by default; set `ModID.game` to `tpf1`, `tf1`, or `transportfever1` for the TPF1 repository.
- Wube: uses the Factorio mod name slug.
- Native bulk lookup is currently implemented for Modrinth, CurseForge, Steam Workshop, mod.io, and transportfever.net.
