Creare un plugin
I plugin estendono XIQUIL senza modificare il core. Un plugin puo aggiungere API FastAPI, tabelle proprie, una pagina React nel menu laterale, pacchetti Python, pacchetti di sistema e hook di lifecycle.
Questa pagina descrive il flusso reale usato dal plugin manager del core: pacchetto .tar.gz, manifest runtime plugin.json installato, eventuale router.py, eventuale plugin.py, eventuale frontend/bundle.js. Per i pacchetti inviati all’Hub, il manifest sorgente consigliato e plugin.source.json.
Quando creare un plugin
Sezione intitolata “Quando creare un plugin”Scegli un plugin quando la funzionalita:
- ha un ciclo di rilascio indipendente dal core;
- richiede dipendenze Python o pacchetti OS non necessari a tutti gli utenti;
- aggiunge una pagina o API specialistiche;
- deve essere distribuita tramite Marketplace;
- puo vivere in tabelle proprie, senza migrazioni Alembic del core.
Scegli invece una modifica al core quando stai cambiando un concetto centrale di XIQUIL, una permission comune, un modello condiviso o un flusso usato da molte aree dell’app.
Dove vive un plugin
Sezione intitolata “Dove vive un plugin”In produzione i plugin installati finiscono in:
/app/data/plugins/├── nome_plugin/│ ├── plugin.json│ ├── router.py│ ├── plugin.py│ ├── frontend/│ │ └── bundle.js│ ├── requirements.txt│ ├── templates/│ └── eventuali_pacchetti_python/└── site/Durante l’installazione XIQUIL:
- estrae il
.tar.gzin staging; - valida
plugin.json; - installa un eventuale
.whlin/app/data/plugins/site; - installa un eventuale
requirements.txtnello stesso site directory; - copia
plugin.json,router.py,plugin.py, pacchetti Python,templates/efrontend/bundle.jsin/app/data/plugins/{name}; - registra eventuali
system_packages; - salva lo stato in database come
installed_pending_restart.
Le modifiche diventano operative dopo un riavvio del server. Se ci sono pacchetti di sistema mancanti, il restart e completo del container; altrimenti viene riavviato solo uvicorn.
Struttura minima
Sezione intitolata “Struttura minima”xq_demo_widget/├── plugin.source.json├── router.py├── plugin.py├── requirements.txt└── frontend/ └── bundle.jsrouter.py serve per esporre API sotto /api/plugins/{name}. plugin.py serve per hook di lifecycle, creazione tabelle e registrazione nav. frontend/bundle.js serve per una pagina React caricata dinamicamente da XIQUIL.
Manifest sorgente
Sezione intitolata “Manifest sorgente”Per upload Hub, plugin.source.json contiene solo i campi tecnici. Versione, descrizione pubblica, tier e compatibilità Core si impostano nel form Hub e verranno inseriti nel plugin.json firmato.
Esempio:
{ "name": "xq_demo_widget", "author": "Il tuo nome", "depends_on": [], "provides_routes": true, "provides_models": false, "provides_frontend": true, "icon": "InfoIcon", "nav_category": "sistema", "nav_path": "/plugins/xq_demo_widget", "permissions": [], "settings_schema": [], "system_packages": []}| Campo | Obbligatorio | Note |
|---|---|---|
name | Si | Identificatore snake_case. Deve combaciare con lo slug registrato sull’Hub |
author | Si | Autore o organizzazione |
depends_on | No | Dipendenze da altri plugin |
provides_routes | No | true se esiste un router.py o un hook register_routes |
provides_models | No | true se il plugin crea tabelle proprie |
provides_frontend | No | true se esiste frontend/bundle.js |
icon | No | Nome icona MUI esposta dal core, ad esempio InfoIcon |
nav_category | No | Gruppo menu laterale, ad esempio fondi, movimenti, fiscalita, sistema |
nav_path | No | Default: /plugins/{name} |
permissions | No | Permessi core che il plugin usa o dichiara |
settings_schema | No | Schema impostazioni future del plugin |
system_packages | No | Pacchetti apt installati al restart completo |
Durante sviluppo locale puoi usare ancora un plugin.json completo per installazioni manuali. Nel Marketplace ufficiale, invece, l’Hub genera il plugin.json finale. Vedi Manifest e metadata plugin.
Non duplicare nel manifest sorgente i campi gestiti dall’Hub: version, display_name, description, required_tier, min_core_version e max_core_version. Se devi cambiare uno di questi valori, aggiornalo nel form Hub della bozza/versione.
Backend: router.py
Sezione intitolata “Backend: router.py”Le route del plugin vengono montate automaticamente sotto /api/plugins/{name}. Se il plugin si chiama xq_demo_widget, l’endpoint /ping qui sotto risponde a /api/plugins/xq_demo_widget/ping.
from datetime import datetime, timezone
from fastapi import APIRouter, Depends
from app.auth.dependencies import LIVELLO_SOLA_LETTURA, require_permission
router = APIRouter()
@router.get("/ping")def ping(): return { "pong": True, "plugin": "xq_demo_widget", "version": "1.0.0", "timestamp": datetime.now(timezone.utc).isoformat(), }
@router.get( "/summary", dependencies=[Depends(require_permission("fondi", LIVELLO_SOLA_LETTURA))],)def summary(): return { "title": "Demo Widget", "items": [ {"label": "API plugin", "value": "attiva"}, {"label": "Permessi core", "value": "fondi: sola lettura"}, ], }Usa le dependency del core per permessi, sessione e utente quando accedi a dati XIQUIL. Un plugin non deve esporre dati cross-ambito senza controllare lo scope dell’utente.
Lifecycle: plugin.py
Sezione intitolata “Lifecycle: plugin.py”plugin.py puo implementare gli hook del Plugin SDK. Per plugin filesystem e upload da Marketplace, XIQUIL fornisce lo shim xq_plugin_sdk con hookimpl.
from xq_plugin_sdk import hookimpl
class XqDemoWidgetPlugin: @hookimpl def on_startup(self, app, db_engine): # Qui puoi creare tabelle plugin_* in modo idempotente. # Evita migrazioni Alembic del core: i plugin gestiscono il proprio schema. pass
@hookimpl def register_routes(self): from . import router return [router.router]
@hookimpl def on_shutdown(self): passPer i plugin filesystem il core monta gia router.py; l’hook register_routes resta utile per compatibilita con il modello Pluggy ed entry point. Le voci menu non si registrano da plugin.py: dichiarale nel manifest con nav_category e nav_path. Tieni gli hook idempotenti: con piu worker il lifecycle puo essere invocato piu volte, anche se il core serializza on_startup dei plugin filesystem con advisory lock PostgreSQL.
Frontend: bundle.js
Sezione intitolata “Frontend: bundle.js”Il frontend del plugin e un modulo ES caricato da:
/plugins/{name}/frontend/bundle.js?v={version}Il bundle non deve includere React, ReactDOM o MUI. Usa i globals esposti dal core in window.__XQ__.
const { React, mui, muiIcons } = window.__XQ__;const { Box, Button, Stack, Typography, Alert } = mui;const { Refresh } = muiIcons;
let apiClient = null;
function DemoWidgetPage() { const [data, setData] = React.useState(null); const [error, setError] = React.useState("");
const load = React.useCallback(async () => { if (!apiClient) { setError("Plugin non ancora inizializzato."); return; }
try { setError(""); const response = await apiClient.get("/api/plugins/xq_demo_widget/summary"); setData(response.data); } catch (err) { setError("Impossibile caricare i dati del plugin."); } }, []);
React.useEffect(() => { load(); }, [load]);
return React.createElement( Stack, { spacing: 2 }, React.createElement( Box, { display: "flex", alignItems: "center", justifyContent: "space-between" }, React.createElement(Typography, { variant: "h5" }, "Demo Widget"), React.createElement( Button, { startIcon: React.createElement(Refresh), onClick: load }, "Aggiorna", ), ), error && React.createElement(Alert, { severity: "error" }, error), data && React.createElement( Stack, { spacing: 1 }, data.items.map((item) => React.createElement( Typography, { key: item.label, variant: "body2" }, `${item.label}: ${item.value}`, ), ), ), );}
export default DemoWidgetPage;
export function setup(context) { apiClient = context.apiClient;}La pagina plugin viene renderizzata da PluginHostPage quando l’utente apre nav_path. Il componente default non riceve props: se ti servono apiClient o navigate, catturali in setup(context) come nell’esempio.
Dipendenze
Sezione intitolata “Dipendenze”Per dipendenze Python pure:
requirements.txthttpx>=0.27python-dateutil>=2.9Per pacchetti OS, dichiara system_packages nel manifest:
"system_packages": [ { "name": "libpango-1.0-0", "optional": false, "description": "Text rendering engine" }]XIQUIL registra i pacchetti e li installa al prossimo restart completo del container. I pacchetti rimossi da un plugin non vengono disinstallati immediatamente: appaiono come orfani nella sezione amministrativa.
Test locale
Sezione intitolata “Test locale”Per testare velocemente in sviluppo:
- avvia XIQUIL in modalita development;
- abilita il tab sviluppatore con una licenza
plugin_developer=trueoppure conPLUGIN_DEVELOPER_MODE=True; - crea la cartella plugin;
- pacchettizza in
.tar.gz; - carica il pacchetto da Marketplace plugin > Sviluppatore > Installa da file;
- riavvia quando richiesto;
- controlla Installati, log e endpoint
/api/plugins/{name}/ping.
Se stai lavorando nella repo xiquil-plugins, puoi usare lo script:
./package.sh xq_demo_widget 1.0.0Lo script usa la versione passata come argomento ed esclude cache e test. Il
tarball mantiene sia plugin.source.json sia plugin.json: il primo
descrive le capability tecniche sottoposte a review, mentre il secondo rende il
pacchetto verificabile e installabile nei flussi locali previsti. Durante
l’approvazione Marketplace l’Hub non si fida dei metadata commerciali presenti
nel file caricato: ricostruisce e sostituisce plugin.json con il manifest
effettivo derivato dai dati revisionati, quindi firma l’archivio normalizzato.
xq_demo_widget-1.0.0.tar.gzChecklist prima della review
Sezione intitolata “Checklist prima della review”plugin.source.jsonvalido;nameuguale allo slug registrato sull’Hub;- versione, tier e compatibilità Core compilati nel form Hub;
- endpoint protetti con permission core adeguate;
- tabelle plugin con prefisso
plugin_{name}_; on_startupidempotente;frontend/bundle.jsnon include React o MUI;- dipendenze Python in
requirements.txto wheel incluso; - pacchetti OS dichiarati in
system_packages; - log utili, senza dati sensibili;
- pacchetto
.tar.gztestato in una istanza pulita.
Esempi reali
Sezione intitolata “Esempi reali”Nel repository dei plugin ufficiali trovi due modelli utili:
xq_bank_reconciliation: API, tabelle proprie, lifecycle hook, frontend React e permessi per ambito;xq_pdf_report: API backend, template, dipendenze Python e pacchetti OS per generazione PDF.
Studiali come riferimento quando devi capire dove mettere router, servizi, modelli, template e bundle frontend.
Prossimi passi
Sezione intitolata “Prossimi passi”- Plugin SDK Python per hook e limiti tecnici;
- Pubblicare su Marketplace per review, firma e canali di rilascio;
- Marketplace plugin per installazione e gestione da UI.