Skip to content

Catalog API

catalog

Load Maraudarr's modular service catalog and resolve stack selections.

CatalogError

Bases: ValueError

Report invalid catalog data or an impossible stack request.

Catalog

Catalog(root: Path | None = None)

Provide validated service metadata, presets, and owned source paths.

Attributes:

Name Type Description
root

Resolved directory containing catalog, template, and service data.

services

Service metadata keyed by stable catalog identifier.

presets

Preset metadata keyed by stable preset identifier.

Load and validate one Maraudarr catalog tree.

Parameters:

Name Type Description Default
root Path | None

Optional catalog root. The environment-aware default is used when this value is absent.

None

Raises:

Type Description
CatalogError

If files, dependencies, or preset references are invalid.

OSError

If the catalog cannot be read from disk.

TOMLDecodeError

If catalog.toml is malformed.

Source code in docker/src/maraudarr/catalog.py
def __init__(self, root: Path | None = None) -> None:
    """Load and validate one Maraudarr catalog tree.

    Args:
        root: Optional catalog root. The environment-aware default is used
            when this value is absent.

    Raises:
        CatalogError: If files, dependencies, or preset references are
            invalid.
        OSError: If the catalog cannot be read from disk.
        tomllib.TOMLDecodeError: If ``catalog.toml`` is malformed.
    """
    self.root = (root or default_catalog_root()).resolve()
    catalog_path = self.root / "catalog" / "catalog.toml"

    with catalog_path.open("rb") as catalog_file:
        data = tomllib.load(catalog_file)

    self.services = {
        service_id: self._load_service(service_id, values)
        for service_id, values in data["services"].items()
    }
    self.presets = {
        preset_id: self._load_preset(preset_id, values)
        for preset_id, values in data["presets"].items()
    }
    self._validate()

preset

preset(preset_id: str) -> Preset

Return a named preset.

Parameters:

Name Type Description Default
preset_id str

Stable catalog identifier for the requested preset.

required

Returns:

Type Description
Preset

The matching immutable preset.

Raises:

Type Description
CatalogError

If the identifier is unknown. The message includes every available preset identifier.

Source code in docker/src/maraudarr/catalog.py
def preset(self, preset_id: str) -> Preset:
    """Return a named preset.

    Args:
        preset_id: Stable catalog identifier for the requested preset.

    Returns:
        The matching immutable preset.

    Raises:
        CatalogError: If the identifier is unknown. The message includes
            every available preset identifier.
    """
    try:
        return self.presets[preset_id]
    except KeyError as error:
        choices = ", ".join(sorted(self.presets))
        raise CatalogError(
            f"Unknown preset '{preset_id}'. Available presets: {choices}."
        ) from error

resolve

resolve(
    preset_id: str,
    add: set[str] | None = None,
    remove: set[str] | None = None,
    selected: set[str] | None = None,
) -> StackPlan

Resolve one preset and service selection into a generation plan.

Parameters:

Name Type Description Default
preset_id str

Preset supplying stack identity and core services.

required
add set[str] | None

Service IDs explicitly added after the starting selection.

None
remove set[str] | None

Optional service IDs removed before additions are applied.

None
selected set[str] | None

Complete starting selection for interactive or custom flows. Preset defaults are used when this value is absent.

None

Returns:

Type Description
StackPlan

An immutable plan containing recursively resolved dependencies in

StackPlan

deterministic catalog order.

Raises:

Type Description
CatalogError

If the preset or a requested service is unknown, or if a custom selection would produce an empty stack.

Source code in docker/src/maraudarr/catalog.py
def resolve(
    self,
    preset_id: str,
    add: set[str] | None = None,
    remove: set[str] | None = None,
    selected: set[str] | None = None,
) -> StackPlan:
    """Resolve one preset and service selection into a generation plan.

    Args:
        preset_id: Preset supplying stack identity and core services.
        add: Service IDs explicitly added after the starting selection.
        remove: Optional service IDs removed before additions are applied.
        selected: Complete starting selection for interactive or custom
            flows. Preset defaults are used when this value is absent.

    Returns:
        An immutable plan containing recursively resolved dependencies in
        deterministic catalog order.

    Raises:
        CatalogError: If the preset or a requested service is unknown, or
            if a custom selection would produce an empty stack.
    """
    preset = self.preset(preset_id)
    requested = set(preset.services if selected is None else selected)
    requested.difference_update(remove or set())
    requested.update(add or set())
    requested.update(preset.core)

    unknown_services = requested - self.services.keys()
    if unknown_services:
        names = ", ".join(sorted(unknown_services))
        raise CatalogError(f"Unknown services requested: {names}.")
    if not requested:
        raise CatalogError("A custom stack must contain at least one service.")

    # Record the user-visible selection before recursively adding required
    # services so the UI can explain which dependencies joined the fleet.
    explicitly_requested = set(requested)
    pending = list(requested)
    while pending:
        service_id = pending.pop()
        for dependency in self.services[service_id].requires:
            if dependency not in requested:
                requested.add(dependency)
                pending.append(dependency)

    ordered_services = tuple(
        sorted(
            (self.services[service_id] for service_id in requested),
            key=lambda service: (service.order, service.id),
        )
    )
    return StackPlan(
        preset=preset,
        services=ordered_services,
        auto_added=tuple(sorted(requested - explicitly_requested)),
    )

source_path

source_path(relative_path: str) -> Path

Resolve a path that must remain inside the catalog root.

Parameters:

Name Type Description Default
relative_path str

Catalog-root-relative source path.

required

Returns:

Type Description
Path

The normalized absolute source path.

Raises:

Type Description
CatalogError

If normalization would escape the owned root.

Source code in docker/src/maraudarr/catalog.py
def source_path(self, relative_path: str) -> Path:
    """Resolve a path that must remain inside the catalog root.

    Args:
        relative_path: Catalog-root-relative source path.

    Returns:
        The normalized absolute source path.

    Raises:
        CatalogError: If normalization would escape the owned root.
    """
    source_path = (self.root / relative_path).resolve()
    if self.root not in source_path.parents and source_path != self.root:
        raise CatalogError(f"Source path escapes Maraudarr root: {relative_path}.")
    return source_path

config_path

config_path(service: Service) -> Path

Return the optional config seed directory for one service.

Parameters:

Name Type Description Default
service Service

Service whose project-owned config seeds are requested.

required

Returns:

Type Description
Path

The normalized path beneath services/<id>/config.

Raises:

Type Description
CatalogError

If the derived source path escapes the catalog root.

Source code in docker/src/maraudarr/catalog.py
def config_path(self, service: Service) -> Path:
    """Return the optional config seed directory for one service.

    Args:
        service: Service whose project-owned config seeds are requested.

    Returns:
        The normalized path beneath ``services/<id>/config``.

    Raises:
        CatalogError: If the derived source path escapes the catalog root.
    """
    return self.source_path(f"services/{service.id}/config")

default_catalog_root

default_catalog_root() -> Path

Locate Maraudarr assets in either the image or source checkout.

Returns:

Type Description
Path

The resolved MARAUDARR_CATALOG_ROOT override when configured;

Path

otherwise, the package's owning docker directory.

Source code in docker/src/maraudarr/catalog.py
def default_catalog_root() -> Path:
    """Locate Maraudarr assets in either the image or source checkout.

    Returns:
        The resolved ``MARAUDARR_CATALOG_ROOT`` override when configured;
        otherwise, the package's owning ``docker`` directory.
    """
    configured_root = os.environ.get("MARAUDARR_CATALOG_ROOT")
    if configured_root:
        return Path(configured_root).resolve()

    return Path(__file__).resolve().parents[2]