|
| 1 | +<!-- FILE AUTO GENERATED BY docs/utils.py DO NOT EDIT DIRECTLY --> |
| 2 | +<a href="https://colab.research.google.com/github/kili-technology/kili-python-sdk/blob/main/recipes/plugins_development.ipynb" target="_parent"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/></a> |
| 3 | + |
| 4 | +# How to develop and test a Kili plugin |
| 5 | + |
| 6 | +## Preliminary |
| 7 | + |
| 8 | +This notebook will teach you how to build your first plugin. |
| 9 | + |
| 10 | +A plugin is an uploaded Python script triggered by an event that you define. |
| 11 | + |
| 12 | +For instance, you can trigger a plugin when a labeler clicks on **Submit** with the `on_submit` handler. |
| 13 | + |
| 14 | +The plugin should have different methods for the different types of events: |
| 15 | + |
| 16 | +- `on_submit` |
| 17 | +- `on_review` |
| 18 | + |
| 19 | +These methods have a predefined set of parameters: |
| 20 | + |
| 21 | +- the `label` submitted |
| 22 | +- the `asset_id` of the asset labeled |
| 23 | + |
| 24 | +Some attributes are available in the class: |
| 25 | + |
| 26 | +- `self.kili` |
| 27 | +- `self.project_id` |
| 28 | + |
| 29 | +Therefore, the skeleton of the plugin should look like this: |
| 30 | + |
| 31 | +```python |
| 32 | +from kili.plugins import PluginCore |
| 33 | +from typing import Dict |
| 34 | +import numpy as np |
| 35 | + |
| 36 | +class PluginHandler(PluginCore): |
| 37 | + """Custom plugin""" |
| 38 | + |
| 39 | + def on_review(self, label: Dict, asset_id: str) -> None: |
| 40 | + """Dedicated handler for Review action""" |
| 41 | + # Do something... |
| 42 | + |
| 43 | + def on_submit(self, label: Dict, asset_id: str) -> None: |
| 44 | + """Dedicated handler for Submit action""" |
| 45 | + # Do something... |
| 46 | +``` |
| 47 | + |
| 48 | +Do not hesitate to reach out to us if you need more. |
| 49 | + |
| 50 | +**NB: The plugin capabilities of Kili are under active development, and compatible with version 2.125.2 and later of Kili. Don't hesitate to reach out via Github or Kili support to provide feedback.** |
| 51 | + |
| 52 | +## Instantiate Kili |
| 53 | + |
| 54 | + |
| 55 | +```python |
| 56 | +%pip install kili |
| 57 | +``` |
| 58 | + |
| 59 | + |
| 60 | +```python |
| 61 | +%load_ext autoreload |
| 62 | +%autoreload 2 |
| 63 | + |
| 64 | + |
| 65 | +from kili.client import Kili |
| 66 | + |
| 67 | +kili = Kili( |
| 68 | + # api_endpoint="https://cloud.kili-technology.com/api/label/v2/graphql", |
| 69 | + # the line above can be uncommented and changed if you are working with an on-premise version of Kili |
| 70 | +) |
| 71 | +``` |
| 72 | + |
| 73 | +## Develop your plugin |
| 74 | + |
| 75 | +The first step is to define the functions that will be called when the event is triggered. You will be able to iterate on these functions locally (more on that in the next section). |
| 76 | + |
| 77 | +The plugin can be defined in two ways: a single `.py` file with everything inside or a module (folder containing multiple `.py` files). In the case of the module type, a file named `main.py` needs to be at the root of the folder and will serve as the entrypoint. |
| 78 | + |
| 79 | +### 1. First option - Plugin defined in a single file |
| 80 | + |
| 81 | +This cell should be the contents of the `.py` file that you will upload as a plugin at the end. |
| 82 | + |
| 83 | +**This file should define the `PluginHandler` class that will contain the proper methods.** |
| 84 | + |
| 85 | +We recommend using a modern IDE like VScode to get type hints and autocompletion on the methods. |
| 86 | + |
| 87 | + |
| 88 | +```python |
| 89 | +import numpy as np |
| 90 | + |
| 91 | +from kili.plugins import PluginCore |
| 92 | + |
| 93 | + |
| 94 | +def custom_function(label: dict): |
| 95 | + label_id = label.get("id") |
| 96 | + print(f"My custom function for review of label with id {label_id}") |
| 97 | + |
| 98 | + |
| 99 | +class PluginHandler(PluginCore): |
| 100 | + """Custom plugin instance""" |
| 101 | + |
| 102 | + def custom_method(self, project_id, label_id): |
| 103 | + print(f"custom_method called for label {label_id}") |
| 104 | + random_seed = np.random.random(1)[0] |
| 105 | + if random_seed > 0.5: |
| 106 | + self.logger.warning("Generating issue") |
| 107 | + # Use kili for actions with self.kili |
| 108 | + self.kili.create_issues( |
| 109 | + project_id=project_id, |
| 110 | + label_id_array=[label_id], |
| 111 | + text_array=["Random issue generated for this label"], |
| 112 | + ) |
| 113 | + |
| 114 | + def on_review(self, label: dict, asset_id: str) -> None: |
| 115 | + """Dedicated handler for Review action""" |
| 116 | + custom_function(label) |
| 117 | + |
| 118 | + def on_submit(self, label: dict, asset_id: str) -> None: |
| 119 | + """Dedicated handler for Submit action""" |
| 120 | + print("On submit called") |
| 121 | + |
| 122 | + project_id = self.project_id |
| 123 | + label_id = label.get("id") |
| 124 | + |
| 125 | + self.custom_method(project_id, label_id) |
| 126 | +``` |
| 127 | + |
| 128 | +### 2. Second option - Plugin defined in a folder |
| 129 | + |
| 130 | +As said previously, the structure of the folder can be the following (the only constraint being the presence of the `main.py` file): |
| 131 | +``` |
| 132 | +plugin_folder |
| 133 | +|__ main.py |
| 134 | +|__ other_file.py |
| 135 | +|__ requirements.txt |
| 136 | +| |
| 137 | +|___helpers |
| 138 | + |__ helper.py |
| 139 | +``` |
| 140 | + |
| 141 | +You can notice that you can also include a `requirements.txt` file in the folder and the necessary packages will be installed with your plugin. Don't forget to add them, since the plugin could work on your machine if you have them installed, but it won't be possible to create the plugin if there are missing dependencies. |
| 142 | + |
| 143 | +**Note:** The `requirements.txt` file can only be included for the SaaS version of the Kili platform, for on-premise deployments there is a pre-defined list of packages that can be used. For more details, see the [documentation of plugins](https://python-sdk-docs.kili-technology.com/latest/sdk/plugins/) |
| 144 | + |
| 145 | +**Important: The main.py file need to have the same skeleton as the plugin defined in a single file (presence of the class `PluginHandler`), the difference being that it can import and call functions defined in other files** |
| 146 | + |
| 147 | +Depending on where the folder is stored, there are two ways to import the plugin in order to test it: |
| 148 | + |
| 149 | +- The first way is to use a relative import (having the plugin folder and the notebook in the same folder). It is simpler and we recommend it as it will also allow the IDE to detect the correct methods and propose hints and autocompletion. |
| 150 | +- The second is to use an absolute path to the plugin folder |
| 151 | + |
| 152 | +#### 2.1 Relative import |
| 153 | + |
| 154 | + |
| 155 | +```python |
| 156 | +# Here replace 'plugin_folder' with the actual name of the folder |
| 157 | +from plugin_folder.main import PluginHandler |
| 158 | +``` |
| 159 | + |
| 160 | +#### 2.2 Absolute path import |
| 161 | + |
| 162 | + |
| 163 | +```python |
| 164 | +import sys |
| 165 | +from pathlib import Path |
| 166 | + |
| 167 | +# Input the path to the plugin folder (it should include the folder), for example '/path/to/plugin_folder' |
| 168 | +plugin_path = "<INSERT PATH TO PLUGIN FOLDER>" |
| 169 | + |
| 170 | +module_path = str(Path(plugin_path).parent.absolute()) |
| 171 | + |
| 172 | +# We are inserting the path in the system PATH to be able to import the module in the next line |
| 173 | +sys.path.insert(0, module_path) |
| 174 | + |
| 175 | +# In the next line replace 'plugin_folder' with the actual name of the folder |
| 176 | +from plugin_folder.main import PluginHandler |
| 177 | +``` |
| 178 | + |
| 179 | +### Testing the plugin locally |
| 180 | + |
| 181 | +In this we will show you how to test your plugin locally before uploading it. |
| 182 | + |
| 183 | + |
| 184 | +```python |
| 185 | +project_id = "<PROJECT ID>" |
| 186 | +``` |
| 187 | + |
| 188 | +Instantiate the plugin: |
| 189 | + |
| 190 | + |
| 191 | +```python |
| 192 | +my_plugin_instance = PluginHandler(kili, project_id) |
| 193 | + |
| 194 | + |
| 195 | +def get_label(label_id, project_id): |
| 196 | + """Function to get the object Label with the same keys as it will be in the plugin""" |
| 197 | + label = list( |
| 198 | + kili.labels( |
| 199 | + project_id=project_id, |
| 200 | + label_id=label_id, |
| 201 | + fields=["id", "jsonResponse", "author.id", "labelType", "createdAt", "secondsToLabel"], |
| 202 | + ) |
| 203 | + )[0] |
| 204 | + |
| 205 | + label["authorId"] = label["author"]["id"] |
| 206 | + del label["author"] |
| 207 | + return label |
| 208 | +``` |
| 209 | + |
| 210 | +### Test the plugin run |
| 211 | + |
| 212 | +If you already have a test project with labels added, you can directly use the IDs of these labels (see the following cell). Otherwise, you can follow the *plugins_example.ipynb* notebook to create a new project and then upload an asset with an associated label. |
| 213 | + |
| 214 | + |
| 215 | +```python |
| 216 | +asset_id = "<YOUR_ASSET_ID>" |
| 217 | +label_id = "<YOUR_LABEL_ID>" |
| 218 | +``` |
| 219 | + |
| 220 | + |
| 221 | +```python |
| 222 | +label = get_label(label_id=label_id, project_id=project_id) |
| 223 | + |
| 224 | +my_plugin_instance.on_submit(label=label, asset_id=asset_id) |
| 225 | +``` |
| 226 | + |
| 227 | +### Test the plugin run on Kili |
| 228 | + |
| 229 | +When you finish debugging the code, you may want to upload it directly into Kili. |
| 230 | + |
| 231 | +Note that you might get an error if the plugin name already exists in your Kili organization. |
| 232 | + |
| 233 | + |
| 234 | +```python |
| 235 | +path_to_plugin = "path/to/my/plugin.py" |
| 236 | +plugin_name = "My first kili plugin" |
| 237 | +``` |
| 238 | + |
| 239 | + |
| 240 | +```python |
| 241 | +from kili.exceptions import GraphQLError |
| 242 | + |
| 243 | +try: |
| 244 | + kili.upload_plugin(path_to_plugin, plugin_name) |
| 245 | +except GraphQLError as error: |
| 246 | + print(str(error)) |
| 247 | +``` |
| 248 | + |
| 249 | +Plugins must be activated in the project that you want them to run in. Be careful with production projects: your custom workflows or rules will also be applied |
| 250 | + |
| 251 | + |
| 252 | +```python |
| 253 | +kili.activate_plugin_on_project(plugin_name, project_id=project_id) |
| 254 | +``` |
| 255 | + |
| 256 | +## Monitoring the plugin |
| 257 | + |
| 258 | +Plugin creation takes some time (around 5 minutes). The plugin will begin to run only after it's been fully created (if labeling events are to be triggered on this project). |
| 259 | + |
| 260 | +Additionally, you can get the logs of the runs: |
| 261 | + |
| 262 | + |
| 263 | +```python |
| 264 | +kili.get_plugin_logs(project_id=project_id, plugin_name=plugin_name) |
| 265 | +``` |
| 266 | + |
| 267 | +You can set custom date rules for filtering your logs: |
| 268 | + |
| 269 | + |
| 270 | +```python |
| 271 | +from datetime import date, datetime |
| 272 | + |
| 273 | +dt = date.today() # You can change this date if needed |
| 274 | +start_date = datetime.combine(dt, datetime.min.time()) |
| 275 | + |
| 276 | +kili.get_plugin_logs(project_id=project_id, plugin_name=plugin_name, start_date=start_date) |
| 277 | +``` |
| 278 | + |
| 279 | +## Managing your plugin |
| 280 | + |
| 281 | +There are several other methods to manage your plugins and their lifecycle. To find out more, check the plugins [tutorials](https://python-sdk-docs.kili-technology.com/latest/tutorials). |
0 commit comments