Getting Started

Set up the Development Environment

  1. Create a new Python directory for your python project to run

  2. Create a virtual env and activate it

    On Mac or Linux terminal:

    $ python3 -m venv venv
    $ source venv/bin/activate
    

    On Windows command line:

    > py -m venv venv
    > venv\Scripts\activate
    
  3. Create a pyproject.toml file

    The pyroject.toml file should have at least a [project] section with the following keys defined:

    • name: This should be the name of your plugin

    • version: The starting version of your plugin

    • dependencies: Include at least “speedwagon”

    For example:

    [project]
    name = "myspeedwagonplugin"
    version = "0.1.0"
    dependencies = [
        "speedwagon"
    ]
    
  4. Create a Python package.

    This should be the same name that you used for the project in the pyproject.toml file. At this point your project should look like this.

    project_root_dir/
    ├── myspeedwagonplugin/
    │   └── __init__.py
    ├── pyproject.toml
    └── venv/
    
  5. Generate plugin hook file with registered_workflows() function.

    There are no strict requirements for the naming of this file outside of ones that Python requires. For my example, I will use hook.py. Create this file inside the Python package folder you just created.

    Use the following code as a starting point.

    # hook.py
    import speedwagon
    from typing import Dict, Type
    
    @speedwagon.hookimpl
    def registered_workflows() -> Dict[str, Type[speedwagon.workflow]]:
        """Register workflows with the plugin.
    
        Returns:
            Returns a dictionary with the name of the workflow for the key and the
            class of the workflow for the value.
        """
        return {}
    

    At this point your project should look like this.

    project_root_dir/
    ├── myspeedwagonplugin/
    │   ├── __init__.py
    │   └── hook.py
    ├── pyproject.toml
    └── venv/
    
  6. Register this hook for Speedwagon

    To register your plugin so that speedwagon can find it, edit the pyproject.toml file by adding a new [project.entry-points.’speedwagon.plugins’] section after the [project] section. Here, provide a name for the workflows and the import path to the hook file containing the registered_workflows() function.

    [project]
    name = "myspeedwagonplugin"
    version = "0.1.0"
    dependencies = [
        "speedwagon==0.3.1"
    ]
    
    [project.entry-points.'speedwagon.plugins']
    myworkflows = 'myspeedwagonplugin.hook'
    
  7. pip install in editing mode

    Use “pip install” with the “-e” flag to install your plugin so that speedwagon can find it while you are developing. Make sure you have activated your virtual env from step 1 first .

    (venv) $ pip install -e .
    

You are done setting up the Speedwagon plugin development environment.

Installing the GUI Framework

Speedwagon does not preinstall the GUI dependency because speedwagon can run without a desktop gui. But if you are developing a plugin, you probably want to install the gui. It’s much easier that way.

$ pip install PySide6
Collecting PySide6
  Obtaining dependency information for PySide6 from https://files.pythonhosted.org/packages/1e/a4/8fd2f8f1d34db1f44a99d4f994e9f81498960161547319b7ce6258acd6bd/PySide6-6.7.0-cp39-abi3-macosx_11_0_universal2.whl.metadata
  Downloading PySide6-6.7.0-cp39-abi3-macosx_11_0_universal2.whl.metadata (5.3 kB)
Requirement already satisfied: shiboken6==6.7.0 in ./venv/lib/python3.12/site-packages (from PySide6) (6.7.0)
Collecting PySide6-Essentials==6.7.0 (from PySide6)
  Obtaining dependency information for PySide6-Essentials==6.7.0 from https://files.pythonhosted.org/packages/5d/81/f64c263851956139cc7012f39d0d174464a2618015962c9ecc82d751330a/PySide6_Essentials-6.7.0-cp39-abi3-macosx_11_0_universal2.whl.metadata
  Downloading PySide6_Essentials-6.7.0-cp39-abi3-macosx_11_0_universal2.whl.metadata (3.7 kB)
Collecting PySide6-Addons==6.7.0 (from PySide6)
  Obtaining dependency information for PySide6-Addons==6.7.0 from https://files.pythonhosted.org/packages/d9/f6/6a95948f729e0f96ba249482b445fca02bf435024f723d59943e2f699942/PySide6_Addons-6.7.0-cp39-abi3-macosx_11_0_universal2.whl.metadata
  Downloading PySide6_Addons-6.7.0-cp39-abi3-macosx_11_0_universal2.whl.metadata (4.0 kB)
Downloading PySide6-6.7.0-cp39-abi3-macosx_11_0_universal2.whl (525 kB)
   ──────────────────────────────────────── 525.3/525.3 kB 4.2 MB/s eta 0:00:00
Downloading PySide6_Addons-6.7.0-cp39-abi3-macosx_11_0_universal2.whl (273.7 MB)
   ──────────────────────────────────────── 273.7/273.7 MB 8.1 MB/s eta 0:00:00
Downloading PySide6_Essentials-6.7.0-cp39-abi3-macosx_11_0_universal2.whl (153.4 MB)
   ──────────────────────────────────────── 153.4/153.4 MB 12.2 MB/s eta 0:00:00
Installing collected packages: PySide6-Essentials, PySide6-Addons, PySide6
Successfully installed PySide6-6.7.0 PySide6-Addons-6.7.0 PySide6-Essentials-6.7.0

Launch speedwagon

$ python -m speedwagon

You should now be able to see and load your plugin within the Speedwagon application.

../_images/plugin_installed.png

Right now the plugin contains no workflows. Let’s fix that.

Build New Speedwagon Workflow

  1. Create a new Python file.

    This can be a file named anything as long as it ends in a .py

  2. Import speedwagon.Workflow

    In the new file, import speedwagon

    # workflows.py
    import speedwagon
    
  3. Create a new class that subclasses speedwagon.Workflow.

    Add a name and description class attributes. The name attribute should be how you want the workflow to list it self. The description field should be a small summary of what it does and explain the workflow parameters

    # workflows.py
    import speedwagon
    
    class DirectoryContentWorkflow(speedwagon.Workflow):
        name = "Show Folder Content"
        description = """Locates the content of a folder
    
    input: path to a directory
    """
    
  4. Add any user input arguments.

    While this step is technically optional, you will most likely need to get some input from the user. To add an input section to the workflow, override the job_options() method.

    class DirectoryContentWorkflow(speedwagon.Workflow):
        ...
        def job_options(self):
            return [
                speedwagon.workflow.DirectorySelect("input"),
            ]
    
  5. Locate information about a job.

    You need to implement discover_task_metadata abstract method for any class derived from speedwagon.Workflow(). This method is for gathering any information that will be used for creating subtasks.

    class DirectoryContentWorkflow(speedwagon.Workflow):
        ...
        def discover_task_metadata(self, initial_results, additional_data, **user_args)
            my_input = user_args['input']
            return [
                {
                    "path": file.path,
                    "name": file.name
                } for file in os.scandir(my_input)
            ]
    
  6. Write a Subtask Class

    Unless your workflow includes any prewritten subtask, you will need to create your own.

    Create a new class the that inherits from speedwagon.tasks.Subtask. The only method that is required is the work() method. You can implement the class however you want.

    class GetFileInformation(speedwagon.tasks.Subtask):
    
        def __init__(self, file_name, file_path):
            self.file_name = file_name
            self.file_path = file_path
    
        def work(self):
            self.log(f"Reading {self.file_name}")
            file_stats = os.stat(self.file_path)
            self.set_results(
                {
                    "name": self.file_name,
                    "size": file_stats.st_size
                }
            )
            return True
    
  7. Assign data located in discover_task_metadata() to the Subtask

    class GetFileInformation(speedwagon.tasks.Subtask):
        ...
        def create_new_task(self, task_builder, **job_args):
            task = GetFileInformation(
                file_name=job_args['name'],
                file_path=job_args['path']
            )
            task_builder.add_subtask(task)
    

Example of a complete Workflow and Subtask full implemented with TypeHints added.

# workflows.py
import speedwagon
import os
from typing import List, Any, Dict, Type

class DirectoryContentWorkflow(speedwagon.Workflow):
    name = "Show Folder Content"
    description = """Locates the content of a folder

    input: path to a directory
    """

    def discover_task_metadata(
        self,
        initial_results: List[Any],
        additional_data: Dict[str, Any],
        **user_args
    ) -> List[dict]:
        my_input = user_args['input']
        return [
            {
                "path": file.path,
                "name": file.name
            } for file in os.scandir(my_input)
        ]

    def create_new_task(self, task_builder: speedwagon.tasks.TaskBuilder, **job_args) -> None:
        task = GetFileInformation(
            file_name=job_args['name'],
            file_path=job_args['path']
        )
        task_builder.add_subtask(task)

    def job_options(self) -> List[AbsOutputOptionDataType]:
        return [
            speedwagon.workflow.DirectorySelect("input"),
        ]

    @classmethod
    def generate_report(
        cls,
        results: List[Result],
        **user_args
    ) -> Optional[str]:
        report_header_lines = [
            f'The content of: {user_args["input"]}',
            ""
        ]

        report_content_lines = [
            f'  * {result.data["name"]} -> size: {result.data["size"]}'
            for result in results
        ]

        return "\n".join(report_header_lines + report_content_lines)


class GetFileInformation(speedwagon.tasks.Subtask):

    def __init__(self, file_name: str, file_path: str) -> None:
        self.file_name = file_name
        self.file_path = file_path

    def work(self) -> bool:
        self.log(f"Reading {self.file_name}")
        file_stats = os.stat(self.file_path)
        self.set_results(
            {
                "name": self.file_name,
                "size": file_stats.st_size
            }
        )
        return True

Register Workflow as Part of Your Plugin

Open the python file that contains the registered_workflows() function. In the dictionary that returns from that function add an entry. This entry should use name of the workflow for the key and the value should be the class to the workflow.

For example:

if your project files are as such…
project_root_dir/
├── myspeedwagonplugin/
│   ├── __init__.py
│   ├── workflows.py
│   └── hook.py
├── pyproject.toml
└── venv/
# hook.py
import speedwagon
from typing import Dict, Type
from myspeedwagonplugin.workflows import SampleWorkflow

@speedwagon.hookimpl
def registered_workflows() -> Dict[str, Type[speedwagon.workflow]]:
"""Register workflows with the plugin.

Returns:
    Returns a dictionary with the name of the workflow for the key and the
    class of the workflow for the value.
"""
    return {
        "My sample workflow": SampleWorkflow
    }