=============== 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: .. code-block:: console $ python3 -m venv venv $ source venv/bin/activate On Windows command line: .. code-block:: doscon > py -m venv venv > venv\Scripts\activate 3) Create a pyproject.toml file The pyroject.toml file should have at least a :strong:`[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" .. _name: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#name .. _version: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#version .. _dependencies: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#dependencies-and-requirements For example: .. code-block:: toml [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. .. code-block:: python # 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 :strong:`[project.entry-points.'speedwagon.plugins']` section after the :strong:`[project]` section. Here, provide a name for the workflows and the import path to the hook file containing the :code:`registered_workflows()` function. .. code-block:: toml [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. :strong:`Make sure you have activated your virtual env from step 1 first .` .. code-block:: console (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. .. code-block:: console $ 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 .. code-block:: console $ python -m speedwagon You should now be able to see and load your plugin within the Speedwagon application. .. image:: plugin_installed.png :width: 500 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 .. code-block:: python # workflows.py import speedwagon 3) Create a new class that subclasses :py:class:`speedwagon.Workflow <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 .. code-block:: python # 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 :py:meth:`job_options() <speedwagon.Workflow.job_options>` method. .. code-block:: python class DirectoryContentWorkflow(speedwagon.Workflow): ... def job_options(self): return [ speedwagon.workflow.DirectorySelect("input"), ] 5) Locate information about a job. You need to implement :py:meth:`discover_task_metadata <speedwagon.Workflow.discover_task_metadata>` abstract method for any class derived from :py:class:`speedwagon.Workflow() <speedwagon.Workflow>`. This method is for gathering any information that will be used for creating subtasks. .. code-block:: python 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 :py:class:`speedwagon.tasks.Subtask <speedwagon.tasks.Subtask>`. The only method that is required is the :py:meth:`work() <speedwagon.tasks.Subtask.work>` method. You can implement the class however you want. .. code-block:: python 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 :py:meth:`discover_task_metadata() <speedwagon.Workflow.discover_task_metadata>` to the :py:class:`Subtask <speedwagon.tasks.Subtask>` .. code-block:: python 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 :py:class:`Workflow <speedwagon.Workflow>` and :py:class:`Subtask <speedwagon.tasks.Subtask>` full implemented with TypeHints added. .. code-block:: python # 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 :code:`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/ .. code-block:: python # 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 }