Skip to content

hpcman.software.recipe

Describes the Recipe class and validates recipes using pydantic

Classes

BaseRecipe

Bases: BaseModel

Source code in hpcman/hpcman/software/recipe.py
class BaseRecipe(BaseModel):  # type: ignore
    model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True)

    recipe_name: NoSpaces
    name: NoSpaces
    method: Annotated[InstallMethod, Field(frozen=True)]
    tags: List[str]
    exes: List[str]
    libs: Optional[List[str]] = None
    category: Category
    docs: Optional[List[AnyUrl]] = None
    refs: Optional[List[AnyUrl]] = None
    install_hook: Optional[str] = None
    help_check: Optional[HelpCheck] = None
    version_check: Optional[VersionCheck] = None
    export: Optional[str] = None
    msgtype: Optional[MsgType] = MsgType.STDOUT  # No longer used.
    patches: Optional[List[str]] = None
    patch_dir: Optional[str] = None
    patch_opts: Optional[str] = None
    patch_order: Optional[PatchOrder] = None

Fields

Name Type Description
recipe_name NoSpaces
name NoSpaces
method InstallMethod
tags List[str]
exes List[str]
libs Optional[List[str]]
category Category
docs Optional[List[AnyUrl]]
refs Optional[List[AnyUrl]]
install_hook Optional[str]
help_check Optional[HelpCheck]
version_check Optional[VersionCheck]
export Optional[str]
msgtype Optional[MsgType]
patches Optional[List[str]]
patch_dir Optional[str]
patch_opts Optional[str]
patch_order Optional[PatchOrder]

CondaRecipe

Bases: BaseRecipe

Source code in hpcman/hpcman/software/recipe.py
class CondaRecipe(BaseRecipe):  # type: ignore
    pkgs: List[str]
    pypi: Optional[List[str]] = None
    channels: Optional[List[str]] = None
    fix_r: bool
    fix_python: bool
    fix_perl: bool
    priority: Optional[Priority] = Priority.STRICT
    cuda: Optional[Cuda] = None

    @field_validator("fix_r", "fix_python", "fix_perl", mode="before")
    @classmethod
    def set_false_if_none(cls, v: Optional[bool], info: ValidationInfo) -> bool:
        if v is None:
            v = False
        return v

Fields

Name Type Description
pkgs List[str]
pypi Optional[List[str]]
channels Optional[List[str]]
fix_r bool
fix_python bool
fix_perl bool
priority Optional[Priority]
cuda Optional[Cuda]

Methods

set_false_if_none

def set_false_if_none(
    v: Optional[bool],
    info: ValidationInfo
) -> bool
Source code in hpcman/hpcman/software/recipe.py
    @field_validator("fix_r", "fix_python", "fix_perl", mode="before")
    @classmethod
    def set_false_if_none(cls, v: Optional[bool], info: ValidationInfo) -> bool:
        if v is None:
            v = False
        return v

PixiRecipe

Bases: BaseRecipe

Source code in hpcman/hpcman/software/recipe.py
class PixiRecipe(BaseRecipe):  # type: ignore
    pkg: str
    channels: Optional[List[str]] = None

Fields

Name Type Description
pkg str
channels Optional[List[str]]

PipxRecipe

Bases: BaseRecipe

Source code in hpcman/hpcman/software/recipe.py
class PipxRecipe(BaseRecipe):  # type: ignore
    pkg: str
    python_ver: Optional[PythonVer] = None
    pipx_backend: Optional[PipxBackend] = PipxBackend.UV

Fields

Name Type Description
pkg str
python_ver Optional[PythonVer]
pipx_backend Optional[PipxBackend]

Functions

load_recipe

def load_recipe(
    recipe_file: Path
) -> BaseRecipe
Source code in hpcman/hpcman/software/recipe.py
@log_call()
def load_recipe(recipe_file: Path) -> BaseRecipe:
    with open(recipe_file, "r") as recipefh:
        logger.trace('yaml = YAML(typ="rt", pure=True)')
        yaml = YAML(typ="rt", pure=True)  # Keeps key order rather than sorting
        logger.trace(f"raw_recipe = yaml.load({recipe_file.as_posix()})")
        raw_recipe = yaml.load(recipefh)
        logger.trace(raw_recipe)
    try:
        raw_recipe["method"]
    except KeyError:
        rprint("[red]Unable to detect install method[/red]")
        rprint(f"Check recipe from {recipe_file.as_posix} and try again")
        exit(1)

    return get_finished_recipe(**raw_recipe)

get_finished_recipe

def get_finished_recipe(
    kwargs = {}
) -> BaseRecipe

Generate proper Recipe Class

Source code in hpcman/hpcman/software/recipe.py
@log_call()
def get_finished_recipe(**kwargs) -> BaseRecipe:
    """Generate proper Recipe Class"""
    method = kwargs.get("method")
    if method == InstallMethod.CONDA:
        return CondaRecipe(**kwargs)
    elif method == InstallMethod.COMPILE:
        return BaseRecipe(**kwargs)
    elif method == InstallMethod.PIPX:
        if "pkgs" in kwargs:
            if len(pkgs := kwargs.pop("pkgs", [])) == 1:
                kwargs["pkg"] = pkgs[0]
            else:
                rprint("[red]Pipx recipe only supports one package[/red]")
                rprint("Either update your recipe with one pkg and/or specify additional pkgs with --libs.")
                exit(1)
        return PipxRecipe(**kwargs)
    elif method == InstallMethod.PIXI:
        if "pkgs" in kwargs:
            if len(pkgs := kwargs.pop("pkgs", [])) == 1:
                kwargs["pkg"] = pkgs[0]
            else:
                rprint("[red]Pixi recipe only supports one package[/red]")
                rprint("Either update your recipe with one pkg or use the conda install method.")
                exit(1)
        return PixiRecipe(**kwargs)
    else:
        rprint(f"[red]Unknown method {method}[/red]")
        return BaseRecipe(**kwargs)

export_and_dump

def export_and_dump(
    recipe_dir: Path,
    recipe: BaseRecipe
) -> Path

Makes directory and dumps recipe there

Source code in hpcman/hpcman/software/recipe.py
@log_call()
def export_and_dump(recipe_dir: Path, recipe: BaseRecipe) -> Path:
    """Makes directory and dumps recipe there"""
    new_recipe = Path(recipe_dir / recipe.recipe_name)
    new_recipe.mkdir(mode=0o775, exist_ok=True)
    dump_model(Path(new_recipe / RECIPE_FN), recipe)
    return new_recipe

dump_model

def dump_model(
    recipe_file: Path,
    recipe: BaseModel,
    readonly: bool = False,
    force: bool = False
) -> None

Dumps recipe to yaml file.

Source code in hpcman/hpcman/software/recipe.py
@log_call()
def dump_model(recipe_file: Path, recipe: BaseModel, readonly: bool = False, force: bool = False) -> None:
    """Dumps recipe to yaml file."""
    writer = YAML(typ="rt", pure=True)
    if force and recipe_file.exists() and not os.access(recipe_file, os.W_OK):
        recipe_file.chmod(stat.S_IWUSR)
    try:
        to_yaml_file(recipe_file, recipe, custom_yaml_writer=writer)
    except PermissionError:
        rprint(f"You do not have permission to over-write {recipe_file.as_posix()}.")
        rprint("Use the '--force' option and try again. Also, check your '$USER'")
        return None
    if readonly:
        recipe_file.chmod(mode=0o444)

get_recipe_list

def get_recipe_list(
    recipe_dir: Path
) -> List[str]

Get list of recipe names.

Source code in hpcman/hpcman/software/recipe.py
def get_recipe_list(recipe_dir: Path) -> List[str]:
    """Get list of recipe names."""
    recipes = [x.name for x in recipe_dir.iterdir() if x.is_dir() and Path(x / RECIPE_FN).exists()]

    return recipes

handle_recipe_errors

def handle_recipe_errors(
    e: ValidationError
) -> None

Handles recipe errors and prints messages

Source code in hpcman/hpcman/software/recipe.py
def handle_recipe_errors(e: ValidationError) -> None:
    """Handles recipe errors and prints messages"""
    rprint(f"Detected {e.error_count()} validation error(s):")
    for error in e.errors():
        if (
            any(option in error["loc"] for option in ("recipe_name", "name"))
            and error["type"] == "string_pattern_mismatch"
        ):
            error["msg"] = "'recipe_name' and 'name' must not contain spaces. Check settings and try again."
        rprint(f" => Fields: {error['loc']}")
        rprint(f" => {error['msg']} -> type={error['type']}, input_value={error['input']}\n")

    rprint("Please provide these values in the hpcrecipe.yaml file or as command-line options and try again.")
    exit(1)

update_recipe

def update_recipe(
    recipe: BaseRecipe,
    kwargs = {}
) -> BaseRecipe

Update recipe with new info

Source code in hpcman/hpcman/software/recipe.py
@log_call()
def update_recipe(recipe: BaseRecipe, **kwargs) -> BaseRecipe:
    """Update recipe with new info"""
    for k, v in kwargs.items():
        if hasattr(recipe, k):
            setattr(recipe, k, v)
        else:
            logger.warning(f"Attempting to set option '{k}' which is not present for method '{recipe.method}'.")
    return recipe

export_model_schema

def export_model_schema(
    method: InstallMethod
) -> None

Exports schema from pydantic model

Source code in hpcman/hpcman/software/recipe.py
def export_model_schema(method: InstallMethod) -> None:
    """Exports schema from pydantic model"""
    if method is InstallMethod.CONDA:
        recipe = CondaRecipe
    elif method is InstallMethod.PIXI:
        recipe = PixiRecipe
    elif method is InstallMethod.PIPX:
        recipe = PipxRecipe
    else:
        rprint(f"Method '{method.value}' hasn't been implemented yet.")
        exit()
    print_json(json.dumps(recipe.model_json_schema()))

load_conda_yaml

def load_conda_yaml(
    yaml_file: FileText
) -> tuple[Channels | None, Packages | None]
Source code in hpcman/hpcman/software/recipe.py
@log_call()
def load_conda_yaml(yaml_file: FileText) -> tuple[Channels | None, Packages | None]:
    yaml = YAML(typ="rt", pure=True)  # Keeps key order rather than sorting
    conda_recipe = yaml.load(yaml_file)
    try:
        channels: Channels | None = conda_recipe["channels"]
    except KeyError:
        channels = None
    try:
        packages: Packages | None = conda_recipe["dependencies"]
    except KeyError:
        packages = None

    return (channels, packages)