Writing Plugins
Plugin Code
A QHAna plugin is a python module or package that contains a class inheriting from QHAnaPluginBase.
The plugin must be placed in a folder specified in the PLUGIN_FOLDERS config variable.
The plugin runner will import all plugins placed in the specified folders.
Only the root module of the plugin will be imported by the plugin runner, the plugin is responsible for importing the plugin implementation class and all celery tasks.
The root modules of plugins must have unique names to avoid problems on import!
A plugin may implement get_api_blueprint() to provide a set of API endpoints.
The returned Bluprint must be compatible with the flask smorest library.
The qhana_plugin_runner.api.util.SecurityBlueprint is recommended for this purpose.
Plugins may implement get_requirements() to specify their requirements to the plugin runner (see Plugin Dependencies).
Plugin API Layout
A plugin should expose the following endpoints with a blueprint:
./for plugin metadata and links to all the plugin endpoints
A plugin that does not follow that schema may not be usable from the QHAna UI later.
Plugin Metadata
Marshmallow schemas to render the plugin metadata can be found in the module plugin_schemas.
Example of plugin metadata:
1{
2 "title": "Plugin Name (display title)",
3 "description": "Human readable description",
4 "name": "plugin-name",
5 "version": "0.0.1",
6 "type": "data-processor",
7 "tags": [
8 "example-tag",
9 "preprocessing",
10 "quantum-algorithm"
11 ],
12 "links": [
13 {
14 "type": "special-link",
15 "href": "./special/"
16 }
17 ],
18 "entryPoint": {
19 "href": "./process/",
20 "uiHref": "./ui/",
21 "pluginDependencies": [
22 {
23 "parameter": "helperPlugin",
24 "type": "processing",
25 "tags": ["my-helper", "!bad-tag"],
26 "required": true
27 },
28 {
29 "parameter": "extraHelperPlugin",
30 "name": "my-helper-plugin",
31 "version": ">=v0.1.0 <=v0.5.0"
32 }
33 ],
34 "dataInput": [
35 {
36 "parameter": "data",
37 "dataType": "entity/list",
38 "contentType": ["application/json", "text/csv"],
39 "required": true
40 },
41 {
42 "parameter": "extra",
43 "dataType": "some-other-type",
44 "contentType": ["*"]
45 },
46 {
47 "parameter": "text",
48 "dataType": "third-type",
49 "contentType": ["text/*"]
50 }
51 ],
52 "dataOutput": [
53 {"dataType": "output-type", "contentType": ["application/json"], "required": true}
54 ]
55 }
56}
Name |
Example |
Description |
|---|---|---|
Title |
My Awesome Plugin |
Human readable title |
Description |
Does something great |
Human readable description |
Name |
my-awesome-plugin |
Stable machine readable name of the plugin. Must be URL-safe! |
Version |
0.0.1 |
A version conforming to <https://www.python.org/dev/peps/pep-0440/#public-version-identifiers> |
Type |
|
A plugin that consumes data and creates new data is a |
Tags |
|
A list of tags describing the plugin. Unknown tags must be ignored while parsing this list. Tags specific to a certain plugin(-family) should be prefixed consistently to avoid name collisions. |
Links |
|
Special links (additional API-level entry points) that are always available outside of a task context. |
Entry Point |
|
The entry point of the plugin. Contains a link to the REST entry point and to the corresponding micro frontend. |
href |
./process/ |
The URL of the REST entry point resource. |
UI href |
./ui/ |
The URL of the micro frontend that corresponds to the REST entry point resource. |
Plugin Dependecies |
|
A list of plugin dependencies. Plugin dependencies can be specified by type (matching the plugin type),
tags (matching the plugin tags; |
Data Input |
|
A list of possible data inputs. Required data inputs must be provided other inputs are optional. The plugin should be selectable once all required data inputs can be provided from the experiment data store. |
Data Output |
|
A list of possible data outputs. Required data outputs will always be produced by the plugin. |
parameter |
data |
The parameter name (or key) under which the input data or plugin reference should be available. |
Data Type |
entity/list |
The data type tag associated with the data. Like content-type but for the data semantic. |
Content Type |
|
Content type (or mimetype) of the data. Describes the encoding of the data. Exactly one of the given content types must match the actual content type of the data. |
When specifying the accepted content or data type of a file input (or output) the following rules should be applied to match the specified type with the actual type:
something,something/,something/*are equivalent and only match anything before the/
*matches anything
application/jsonis an exact match
Visualization Plugin Micro Frontend
A visualization plugin defines both href and hrefUi to point to the micro frontend that provides the data visualization.
The endpoint must accept a single query parameter data-url in the URL.
The accepted data type can be indicated by specifying a required dataInput.
A visualization plugin must have exactly one required data input or exactly one data input (that is implicitly assumed as required).
A visualization plugin must not produce any new data and must not list any data outputs.
Note
The specification of visualization plugins is WIP and will be finished later.
Processing Plugin Micro Frontend
All QHAna plugins should expose the parameters of the algorithm in a micro frontend (see Use MICRO-Frontends to Expose Algorithm Parameters for reasoning). The micro frontends should only use html and css. Javascript can be used but should be used sparingly to ease the integration of the micro frontend into the QHAna UI later.
The parameters must be defined inside a native html form. Starting the algorithm with the parameters must be done through a form submit button.
The plugin runner contains template macros that can be imported and used to auto generate form elements from simple marshmallow schemas.
1{% import 'forms.html' as forms %}
2
3<!-- process is the url of the processing resource, values the current form data or query data and errors are validation errors from marshmallow -->
4{% call forms.render_form(method='post') %}
5 <!-- schema is the marshmallow schema and values is a dict containing prefilled (and serialized) values -->
6 {{ forms.render_fields(schema, values=values, errors=errors) }}
7 <div class="qhana-form-buttons">
8 {{ forms.submit("validate")}} <!-- validate form by sending it to the ui endpoint (should keep form inputs intact!) -->
9 {{ forms.submit("submit", action=process)}} <!-- submit data to processing resource -->
10 </div>
11{% endcall %}
Communication With the Pluin Host
The Micro Frontends are loaded inside iframes. This means that they are sandboxed from the parent window and need to communicate via messaging. For this purpose a generic microfrontend.js is included in the static folder that is also part of the simple template. Plugins that want to use this script should use the attributes described in the next section.
The messages that can be exchanged with the plugin host are documented in an AsyncAPI document.
The document can be found here asyncapi.json.
To view the document use the AsyncAPI studio.
Custom Attributes used in Micro Frontends
The Micro Frontend can use a number of custom html attributes to mark some inputs for the QHAna frontend to be enhanced. This can be used to mark data input fields for the QHAna frontend.
Attribute |
Example |
Description |
|---|---|---|
|
entity |
Mark an input field as data input. The QHAna UI can choose to instrument the input with a datalist of possible data entries or with a data selection dialog. |
|
|
A list of acceptable content types seperated by a space. |
|
|
Mark a submit button (or input) as validating or submitting. A validating button must point to a resource returning a validated micro frontend (possibly with extra error messages). A submitting button must point to the REST resource corresponding to the micro frontend. If this attribute is missing or unspecified a heuristic should be used to determine the type of the submit button. |
|
|
Mark a password input as an API token input. The value specifies for which API the token will be used. |
|
Mark an input as private. Values of private inputs must never be stored in permanent storage by QHAna. Password inputs are considered private by default. |
Processing Plugin Results
The REST entry point of a plugin must return (or forward to) a valid plugin result value.
Example of a plugin result:
1{
2 "status": "PENDING",
3 "log": "…",
4 "progress": {
5 "value": 100,
6 "start": 0,
7 "target": 100,
8 "unit": "%"
9 },
10 "steps": [
11 {
12 "href": ".../<UUID>/step1-process",
13 "uiHref": ".../<UUID>/step1-ui",
14 "stepId": "step1",
15 "cleared": true
16 },
17 {
18 "href": ".../<UUID>/step2b-process",
19 "uiHref": ".../<UUID>/step2b-ui",
20 "stepId": "step1.step2b",
21 "cleared": true
22 }
23 ],
24 "links": [
25 {
26 "type": "special-link",
27 "href": "./task/<UUID>/special/"
28 }
29 ],
30 "outputs": [
31 {
32 "href": ".../<UUID>/data/1",
33 "dataType": "entity/list",
34 "contentType": "application/json",
35 "name": "EntityList"
36 }
37 ]
38}
Name |
Example |
Description |
|---|---|---|
Status |
|
The current state of the result. |
Log |
Step 1: Finished processing 125 entities in 1.2 seconds. |
Some human readable log of the result calculation. Use this field to convey errors that happened during the result calculation. |
Progress (optional) |
|
An object describing the current progress of the result calculation. |
Steps (optional) |
|
A (growing) list of sub-steps that need new (user-) input before the final result can be computed.
Only the last step in the list can be marked with |
Links |
|
Special links (additional API-level entry points) that are only available in a task context. |
Outputs |
|
The list of data that was produced for this result. Must only be present on |
Result Progress
The result progress object can be used to indicate the current progress of a pending result.
If no progress object is given the progress is assumed to be indeterminate (e.g. a progress spinner should be displayed).
If a progress object is given then the progress can be displayed to the user (e.g. in form of a progress bar or a x/100 % counter).
Name |
Example |
Description |
|---|---|---|
Value |
70 |
The current progress value. Must be a number between |
Start |
0 |
The starting progress value. Defines the point of no progress. Must be a number.
If |
Target |
100 |
The target progress value that defines all work beeing finished. Must be a number. Defaults to |
Unit (optional) |
% |
The unit the progress is given in. Can be used to display the progress to the user. Defaults to |
Result Steps
Result steps are intermediate steps where additional input is required to continue the result computation.
The list of result steps should only grow with new steps added on the end of the list.
Only the last step should be active (e.g. not marked as cleared). Plugins that use multiple steps should store form inputs as usual in parameters. Data that is used in subsequent steps should then be extracted in the respective celery task and stored in the key-value store data that has dict-like functionality. Furthermore, whenever valid input data for the current uncleared step is available, clear_previous_step must be called in the function that handles the input data (i.e., the processing endpoint for the corresponding microfrontend endpoint).
Name |
Example |
Description |
|---|---|---|
href |
http(s)://…/<UUID>/step1 |
A link to the REST resource accepting the input data for the step. This URL must be an absolute URL containing schema and host! |
UI href |
http(s)://…/<UUID>/ui-step1 |
A link to the micro frontend corresponding to the REST resource accepting the input data for the step. This URL must be an absolute URL containing schema and host! |
Step ID (optional) |
step1.step2b |
A stable id corresponding to the current branch of the result computation. The same choices in previous steps with the same data should always produce the same step id. The step id may be completely independent from the input data. The step id may be used to reliably repeat a recorded plugin interaction (or detect when the recorded interaction deviates from the current one). |
Cleared |
|
A flag indicating that the step has already accepted input and can be considered as cleared. Defaults to |
Result Data
The final result data is represented by a list of links to the data element. The list must not be present until the result is completed.
Name |
Example |
Description |
|---|---|---|
href |
…/<UUID>/data/1 |
The URL where the (raw) data can be accessed. |
Name |
FilteredEntityList |
A human readable name given to the output data by the plugin. Should fit the data content. |
Content Type |
application/json |
The content type (mimetype) of the data. Describes how the data is encoded. |
Data Type |
entity/list |
The data type tag associated with the data. Describes what kind of data is encoded. Must not contain wildcards ( |
Conversion Plugins
Conversion plugins are special processing plugins. The intended purpose of conversion plugins is to allow automatic conversion between different serialization formats.
Note
The specification of conversion plugins is WIP and will be finished later.
Plugin Dependencies
Plugins can declare their external python dependencies by implementing the get_requirements() method.
The method must return the requirements in the same format as requirements.txt used by pip.
See also
Requirements.txt format: https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format
The plugin requirements of the loaded plugins can be installed using the plugin cli.
Important
The installation will fail if any requirement cannot be satisfied. This includes the pinned requirements of the plugin runner itself!
Plugin resolution may also take an exceptionally long time if the requirements have conflicting versions. Make sure that the plugin requirements are actually compatible with the plugin runner requirements.
Note
The requirement install mechanism is currently experimental and relies on the pip resolver. This means that resolving complex requirement sets can take a very long time. Plugins should therfore minimize their requirements and (whenever possible) only depend on requirements installed by the plugin runner already. Requirements of the plugin runner should not be part of the requirements the plugin specifies itself.
Warning
Plugins must fail gracefully if their dependencies are not yet installed.
If the plugin does not fail gracefully the plugin runner cannot get the plugin requirements by calling get_requirements().
This also means that it cannot install the requirements for that plugin!
Strategies for Plugins With External Dependecies
Plugins with external dependencies must fail gracefully if their dependencies are not installed. Otherwise they cannot inform the plugin runner about their dependencies.
Late Imports of Dependencies
Instead of importing dependencies at the top of the module import your dependency locally (i.e. in the celery task instead of in the module). This allows the plugin to load while the failing import does not get executed until the task is called.
This method is useful for one-module plugins that rely on external dependencies for specific calculations/functionality.
Catch import Errors
Surround the failing import with try-except and handle cases where the import failed gracefully.
A failing import can produce NameErrors when code tries to use the imported names.
This method is useful for one-module plugins that rely on external dependencies for specific calculations/functionality.
Reorganize Code
If the external dependency is tightly integrated into your plugin (e.g. through type hints) then it is best to move all code depending on the external functions into its own module or package. This means that your plugin should be a python package! Then one of the above techniques can be used to import that package.
Import in get_api_blueprint() method
This is a combination of all the above strategies.
The import happens late in the get_api_blueprint() method of the plugin.
To fail gracefully the import is guarded with a try-except statement.
The method is allowed to throw a NotImplementedError when the plugin does not provide a blueprint.
from qhana_plugin_runner.util.plugins import QHAnaPluginBase
...
class MyPlugin(QHAnaPluginBase):
name = "my-plugin"
description = "A plugin description."
tags = ["tag"]
version = "1.0"
def __init__(self, app: Optional[Flask]) -> None:
super().__init__(app)
def get_api_blueprint(self):
try:
# late import, code was reorganized into submodule
from .code_with_dependencies import MY_PLUGIN_BLP
return MY_PLUGIN_BLP
except ImportError:
# fail gracefully with try-except block
raise NotImplementedError("Plugin dependencies not installed.")
Long Running Tasks
Long running tasks can be implemented using Celery tasks.
Task names should be unique.
This can be achieved by using the plugin name as part of the task name.
If a background task is started from a processing resource it must be registered in the database as a processing task (see plugins/hello_world.py).
There are some utility tasks that can be used in the tasks module.
File Inputs
Plugins should load files from URLs (see ADR Always Pass Files as URLs).
The plugin runner provides a utility method (open_url()) for accessing http(s)://, file:// and data: URLs.
If the plugin accepts large files then the URL should be opened with stream=True and the data should be read incrementally if possible.
This can reduce the memory footprint of the plugin.
Data formats for input files (especially those used by multiple plugins) should be specified in Data Formats defined for QHAna Plugins. The plugin runner has builtin support for some formats, e.g. the ones specified in Data Loader Formats.
See also
The plugin utils module for marshalling entity data: qhana_plugin_runner.plugin_utils.entity_marshalling
Loading Entities
The plugin runner provides various utility functions to load entity data.
The function load_entities() can be used to load entities.
To ensure a dict or tuple output type for entities use the functions ensure_dict() and ensure_tuple() respectively.
Entities of type entity/vector can be preprocessed with the ensure_array() function.
Warning
If the plugin accepts entitiy data serialized as text/csv, then the plugin should also accept an (optional) entity/attribute-metadata input.
The attribute metadata can be used to preprocess the entity data, e.g., convert numbers to numeric data types or split list like values into lists.
This can be achieved by first creating a deserializer with tuple_deserializer() or dict_deserializer() and second using that serializer on each tuple or dict.
Use parse_attribute_metadata() to parse the attribute metadata entities.
File Outputs
Plugins can use the FileStore STORE to persist intermediate files and result files.
The storage registry will forward methods to the configured default FileStore.
The plugin runner come with a file store implementation that uses the local filesystem as backend.
The final results of a task should be stored in the file store using the persist_task_result() method.
If a task produces large intermediate results that have to be shared to following tasks then these results should be stored as a file using the persist_task_temp_file() method.
The TaskFile instance returned by that method should not be shared directly between tasks.
Instead share the id attribute and retrieve the task file info with get_by_id().
The files can be retrieved from the file store by requesting an URL for the file information.
Use get_task_file_url() for task files and get_file_url() for other files.
Tasks can use the internal URLs provided by these methods (set external=False) while file downloads from outside of the plugin runner must use the external URLs.
Data formats for output files should be specified in Data Formats defined for QHAna Plugins. The plugin runner has builtin support for some formats, e.g. the ones specified in Data Loader Formats. When writing a new plugin that outputs data first consider using an already specified output format before creating your own. This will increase the chance that other plugins can work with that data seamlessly.
See also
The plugin utils module for marshalling entity data: qhana_plugin_runner.plugin_utils.entity_marshalling
Saving Entities
The plugin runner provides various utility functions to save entity data.
The function save_entities() can be used to save entities.
Entities of type entity/vector can be preprocessed with the array_to_entity() function.
Warning
If the plugin saves new entitiy data (or modifies entity data), then the plugin should also provide an entity/attribute-metadata output describing the attributes of the entitites.
The attribute metadata can be used by other plugins to preprocess the entity data, e.g., convert numbers to numeric data types or split list like values into lists.
It can also be used by the plugin creating the entity data to ensure that all entities get serialized correctly.
This can be achieved by first creating a serializer with tuple_serializer() or dict_serializer() and second using that serializer on each tuple or dict.
Use to_dict() to serialize the AttributeMetadata objects themselves.
Writing Plugin Tests
Plugin tests are co-located with the plugin code, in either a nested layout (plugins/<name>/tests/test_*.py) or a flat layout (plugins/<name>/test_*.py). See Co-locate plugin tests with plugin code for the rationale and Writing Tests for a worked example of each layout.
The shared task_data fixture and the helpers from tests/utils.py are reusable from plugin tests with no extra setup.
Celery task tests run against an in-process worker on an in-memory broker, with no Redis or Docker required (see Celery Task Testing Strategy).
A few points to keep in mind when adding tests for a plugin:
Plugin source files must use relative imports so plugins remain relocatable. Test files are excluded from this check and may use absolute imports.
Test module names can collide across plugins. Pytest’s
--import-mode=importlibhandles the disambiguation.For Celery tasks, import the plugin’s tasks at module level in the test file so the
CELERYsingleton picks up the registration before the worker fixture starts.
See Writing Tests for the pytest configuration, the full fixture catalogue, the Celery testing pattern, and how tests run in CI.