Launcher Plugins

Kloch comes with a basic plugin system that allow you to add new launchers. The workflow can be simply described as:

  • create a new python module at an arbitrary location

  • subclass BaseLauncher and BaseLauncherSerialized inside

  • make sure its parent location is registred in the PYTHONPATH so they can be imported

  • append the module name to the launcher_plugins configuration key

Which could be illustrated by the following bash commands:

cd /d/dev/my-kloch-plugins/
touch kloch_mylauncher.py
# edit its content ^
export PYTHONPATH=$PYTHONPATH:/d/dev/my-kloch-plugins/
export KLOCH_CONFIG_LAUNCHER_PLUGINS=kloch_mylauncher
# check if we succesfully registred our plugin
kloch plugins

As example you can check:

Creating the module

A plugin for kloch is a regular python module that will be imported and parsed to find specific object.

So you can create a regular python file at any location, or if you prefer you can also ship your plugin as a python package by creating a directory and an __init__.py file.

Tip

As kloch plugins need to be in the PYTHONPATH, every python code will be able to import them so make sure to pick a name that will not conflict with other modules. Prefixing them with kloch_ is recommended.

Creating the subclasses

When creating the a new launcher you start by creating a dataclass object that declare what are the user-configurable options and how are they “launched”.

This is done by subclassing BaseLauncher.

Next you need to declare how that dataclass must be serialized, by creating a subclass of BaseLauncherSerialized. This act as a high-level controller for serialization.

For the granular control over how each field of the dataclass is serialized you must created another dataclass subclass, but of BaseLauncherFields. Which will just miror the BaseLauncher field structure, but where each of its field provide more metadata to unserialize the field.

Here is an example which subclass both of those class, in which we create a launcher for “git cloning”:

kloch_gitclone.py
import dataclasses
import http.client
import subprocess
from pathlib import Path
from typing import List
from typing import Optional

from kloch.launchers import BaseLauncher
from kloch.launchers import BaseLauncherSerialized
from kloch.launchers import BaseLauncherFields


@dataclasses.dataclass
class GitCloneLauncher(BaseLauncher):

    name = "git-clone"

    # all field must have a default value, but we make it
    #   required using the bellow `required_fields` class attribute
    remote_url: str = ""

    required_fields = ["remote_url"]

    def execute(self, tmpdir: Path, command: Optional[List[str]] = None):
        # we consider `command` are just extra args to git clone command
        _command = ["git", "clone", self.remote_url] + command
        subprocess.run(_command, env=self.environ, cwd=self.cwd)


@dataclasses.dataclass(frozen=True)
class GitCloneLauncherFields(BaseLauncherFields):

    # field name must be the same as in the BaseLauncher subclass above
    remote_url: str = dataclasses.field(
        # this is the expected key name in the serialized representation
        default="remote-url",
        # this if for automated documentation generation
        metadata={
            "description": "An URL to a valid remote git repository.",
            "required": True,
        },
    )


def does_url_exists(url: str) -> bool:
    connection = http.client.HTTPConnection(url)
    connection.request("HEAD", "")
    return connection.getresponse().status < 400


class GitCloneLauncherSerialized(BaseLauncherSerialized):
    # the class it serialize
    source = GitCloneLauncher

    # we can pick a different name but we keep it similar for simplicity
    identifier = GitCloneLauncher.name

    fields = GitCloneLauncherFields

    # short one line description of the launcher
    summary = "Just clone a repository from a git remote."

    # full documentation of an arbitrary length for the launcher
    description = "From a git remote repository url, git clone it in the current working directory."

    def validate(self):
        super().validate()
        remote_url = self.fields.remote_url
        assert remote_url in self, f"'{remote_url}': missing or empty attribute."
        assert does_url_exists(
            self[remote_url]
        ), f"'{remote_url}': url provided doesn't exists: {self[remote_url]}."

    # we override for type-hint
    def unserialize(self) -> GitCloneLauncher:
        # noinspection PyTypeChecker
        return super().unserialize()

Tip

When implementing a field for the user to have control on the launcher, it’s best to implement it as dict type over list because it allow users to override one item in particular using the token system.

Registering

Then add the kloch_gitclone.py parent location in your PYTHONPATH so the plugin system can do an import kloch_gitclone.

Tip

PYTHONPATH is the standard mechanism by python to discover and import modules but nothing prevent you to use other tools or methods.

You could for example create a pyproject.toml which declare kloch and your plugin as dependency and let a tool like uv or poetry create the venv.

As long as it can be import my_plugin_module_name it will work !

The last step is to add the module name in the list of launcher plugins to use. You do this by modifying the kloch configuration, which is explained in Configuration

A quick way to do it is just to set the corresponding environment variable before starting kloch.

export KLOCH_CONFIG_LAUNCHER_PLUGINS=kloch_gitclone

You can check your plugin is registred by calling:

kloch plugins