Everything a plugin can declare, receive and do, in plugin API version 1. For a walk-through, read Writing a plugin first.
Pythonator's background service the plugin's own process (sandboxed)
┌──────────────────────────────┐ JSON lines ┌───────────────────────────────────┐
│ plugin host │ ─────────────▶ │ worker: loads plugin.py, │
│ · checks every message │ stdin │ calls your on_* methods │
│ · applies permissions │ ◀───────────── │ │
│ · rate limit, size limit │ stdout │ self.notify(), self.show(), ... │
└──────────────────────────────┘ └───────────────────────────────────┘
Pythonator starts Pythonator --plugin-worker <plugin folder> <data folder> inside the same sandbox services use,
with network only if the plugin was granted network. The worker imports your file (compiling it fresh, never using
.pyc files shipped with it), sends hello, and then calls your methods as messages arrive, one at a time.
Your code never sees Pythonator's objects, only copies of the data your permissions allow.
[plugin]
id = "my-plugin" # required: a-z, 0-9 and -, 2 to 41 characters, starting with a letter
name = "My plugin" # required: shown everywhere, up to 64 characters
version = "1.2.0" # required: 1 to 4 numbers, optional suffix (1.2.0-beta1)
api = 1 # the plugin API it was written for; a newer one than Pythonator's is refused
entry = "plugin.py" # the Python file, inside the plugin folder
description = "..." # up to 300 characters
permissions = ["network"] # see below
author = "you"
homepage = "https://..."
[[plugin.settings]] # any number, up to 40
key = "interval" # a-z, 0-9 and _, starting with a letter
label = "Seconds between checks"
kind = "int" # text, secret, bool, int, choice, lines
default = 30
choices = [] # for kind = "choice"
help = "Shown under the field."The file must be under 64 KB. Unknown keys are refused, so a typo is an error, not a silent no-op. Nothing in the plugin is imported to read it.
| Name | Gives |
|---|---|
network |
Network in the plugin's sandbox |
read_logs |
on_output calls with the services' output |
inspect |
cpu, memory, listening and remote in ServiceInfo, and on_stats calls |
control |
start, stop, restart work (in test mode they are recorded instead) |
policy |
check_start is asked before each start (in test mode a refusal is only recorded) |
ui |
show displays a panel |
Official plugins get what they declare. Third-party plugins get only what the user grants, which can be less.
self.permissions holds what this run was granted, and self.test_mode whether it is in test mode.
from pythonator.sdk import Plugin, ServiceEvent, ServiceInfo, uiDefine exactly one subclass of Plugin in your entry file.
| Attribute | Type | Holds |
|---|---|---|
settings |
dict[str, Any] |
The settings, defaults filled in |
services |
dict[str, ServiceInfo] |
Every service by id, kept up to date |
permissions |
frozenset[str] |
Permissions granted for this run |
test_mode |
bool |
Whether actions are only recorded |
data_dir |
Path |
A private folder for the plugin's own files; the only place it can write |
| Method | Called |
|---|---|
on_start() |
Once, after settings and services are filled in. Start your threads here |
on_stop() |
Before the plugin is stopped. Keep it short |
on_event(event) |
A service started, stopped, crashed... (see the kinds below) |
on_output(service, lines) |
New output of a service, up to 200 lines at a time (read_logs) |
on_stats() |
Every 2 seconds, after services got new CPU, memory and ports (inspect) |
check_start(service) -> str | None |
Before a start: return a reason to refuse it (policy); answer within 2 s |
on_settings() |
The user saved new settings |
on_action(action, value) |
A button of your panel was pressed |
on_tick() |
About every 5 seconds |
An exception in any of them is reported in the plugin's Activity with its traceback, and the plugin goes on.
| Method | Does | Needs |
|---|---|---|
log(text) |
A line in the plugin's Activity | |
notify(text, level="info") |
A notice in the window; level is info, ok, warn or error |
|
show(panel) |
Shows or replaces your panel | ui |
start(id), stop(id), restart(id) |
Acts on a service | control |
They are safe to call from any thread.
Read-only. The fields under inspect are zero or empty without that permission.
| Field | Type | |
|---|---|---|
id, name |
str |
|
runtime |
str |
python, node, go... or custom |
mode |
str |
sandbox, container or full |
state |
str |
stopped, starting, running, stopping, waiting, failed, task |
network |
bool |
May use the network |
ports |
tuple[int, ...] |
The ports it declares |
expose |
str |
local or lan |
memory_limit_mb |
int |
0 for none |
restarts |
int |
Restarts in the last 10 minutes |
cpu |
float |
inspect: percent of one core |
memory |
int |
inspect: bytes, whole process tree |
listening |
tuple[int, ...] |
inspect: ports it really listens on |
remote |
tuple[str, ...] |
inspect: address:port it is connected to, up to 50 |
| Field | |
|---|---|
kind |
started, stopped, exited, crashed, gave_up, blocked, installing, installed, install_failed |
service |
The ServiceInfo after the event |
exit_code |
For exited and crashed |
note |
A short explanation, for example the reason of a blocked start |
Build panels with ui. They are data, checked by Pythonator and drawn with native widgets: text is always shown as
text, never as HTML.
| Builder | Shows |
|---|---|
ui.panel(title, *items) |
The panel: a title (up to 80 characters) and up to 200 items |
ui.text(text, tone="") |
A paragraph, up to 4000 characters |
ui.stat(label, value, tone="") |
A small card with a label and a big value |
ui.table(columns, rows, tones=None) |
Up to 12 columns and 500 rows; tones colours each row |
ui.button(label, action, value="") |
A button that calls on_action(action, value); action is letters, digits and _.:- |
ui.row(*items) |
Items side by side, up to 20 |
A tone is "", ok, warn, error or muted. A panel holds at most 500 items in all. A panel that breaks these
rules is refused, with the reason in the Activity, and the previous panel stays.
| Limit | Value | What happens |
|---|---|---|
| Messages per second | 200, bursts of 400 | Extra messages are dropped; after 2000 dropped the plugin is disabled |
| Message size | 64 KB | The plugin is disabled |
| Not a protocol message | The plugin is disabled | |
| Crashes | 3 in 10 minutes | The plugin is disabled; before that it restarts after 2 s, then 4 s |
check_start |
2 s | The start goes ahead without the plugin's answer |
You don't need this to write a plugin in Python; the SDK speaks it for you. One JSON object per line, UTF-8, with
the message type in t.
From Pythonator to the plugin (on its stdin):
t |
Fields |
|---|---|
init |
settings, permissions, test_mode, services (a list of service views) |
event |
kind, exit, note, service |
services |
services: the list changed |
output |
id, lines |
stats |
services with CPU, memory and ports |
check |
req, service: answer with verdict |
settings |
settings |
action |
action, value |
tick |
|
stop |
From the plugin to Pythonator (on its stdout):
t |
Fields |
|---|---|
hello |
Sent once when loaded; Pythonator answers with init |
log, error |
text |
notify |
text, level |
panel |
panel |
control |
op (start, stop, restart), id |
verdict |
req, deny (empty to allow) |
The worker sends the protocol on the real stdout and redirects print() to stderr, which becomes the Activity.
The API version changes only when a change would break existing plugins. New optional fields and new methods don't
change it. Pythonator refuses a plugin with a newer api than its own, with a message saying it needs a newer
Pythonator.