Salta ai contenuti

Plugin SDK Python

Il Plugin SDK v1 di XIQUIL e basato su Pluggy e su un loader filesystem. Il contratto pubblico e piccolo e versionato: un plugin installato dichiara cosa offre nel plugin.json, puo esporre route FastAPI, implementare hook di lifecycle e fornire un bundle frontend.

Il repository Core contiene i sorgenti canonici degli SDK:

  • sdk/python, pacchetto xiquil-plugin-sdk con marker Pluggy, PluginHooks, PluginFacade e type marker py.typed;
  • sdk/typescript, pacchetto types-only @xiquil/plugin-sdk per il modulo frontend, il contesto host e window.__XQ__;
  • sdk/examples, con esempi backend-only, frontend-only e full-stack.

Finche gli artefatti non saranno pubblicati nei registry, usa la versione proveniente dalla release Core corrispondente. Non copiare tipi o marker in un SDK privato: i test Core verificano che il contratto pubblico non derivi dall’implementazione runtime.

All’avvio XIQUIL:

  1. aggiunge /app/data/plugins/site al sys.path;
  2. scansiona /app/data/plugins/ cercando sottocartelle con plugin.json;
  3. carica eventuali entry point Python del gruppo xq-plugins;
  4. filtra i plugin in base a licenza, feature e stato database;
  5. registra le route abilitate sotto /api/plugins/{name} e, nella stessa fase, carica eventuali classi hook da plugin.py dei plugin filesystem;
  6. invoca gli hook on_startup;
  7. espone al frontend il manifest tramite /api/system/plugins/manifest.

Il frontend usa quel manifest per mostrare le voci di menu e caricare /plugins/{name}/frontend/bundle.js.

Nei nuovi plugin importa hookimpl dal pacchetto pubblico:

from xiquil_plugin_sdk import PluginFacade, hookimpl

Lo shim storico xq_plugin_sdk resta compatibile con i plugin esistenti, ma non e il punto di ingresso consigliato per nuovo codice.

Restituisce una lista di APIRouter. Le route vengono montate sotto /api/plugins/{plugin_name}.

@hookimpl
def register_routes(self):
from . import router
return [router.router]

Per i plugin filesystem, il core supporta anche il caricamento diretto di router.py. Mantieni comunque register_routes se vuoi compatibilita con plugin installati come entry point.

Eseguito quando FastAPI avvia l’applicazione e il database engine e disponibile.

@hookimpl
def on_startup(self, app, db_engine):
from sqlmodel import SQLModel
from .models import PluginDemoRecord
SQLModel.metadata.create_all(
db_engine,
tables=[PluginDemoRecord.__table__],
)

Usalo per inizializzare risorse e creare tabelle del plugin. Le operazioni devono essere idempotenti: create_all() non aggiunge colonne a tabelle gia esistenti, quindi le migrazioni additive vanno gestite esplicitamente con controlli su colonne e indici.

Eseguito in chiusura applicazione.

@hookimpl
def on_shutdown(self):
pass

Usalo per rilasciare risorse non gestite automaticamente.

Questo hook e presente nella spec Pluggy, ma il core attuale non lo raccoglie con un call site dedicato. Non usarlo come meccanismo di navigazione.

Le voci nel menu laterale vengono costruite dal manifest frontend restituito da /api/system/plugins/manifest. Nei pacchetti pubblicati via Hub, display_name arriva dal form Hub mentre icon, nav_category e nav_path arrivano dal manifest tecnico.

{
"display_name": "Demo Widget",
"provides_frontend": true,
"icon": "InfoIcon",
"nav_category": "sistema",
"nav_path": "/plugins/xq_demo_widget"
}

Hook avanzato per sostituire il motore previsionale del modulo Previsione.

@hookimpl
def register_simulation_engine(self):
return {
"name": "monte_carlo",
"run": run_scenarios,
}

La callable run(context) riceve dati gia preparati dal core e deve restituire una lista compatibile con lo schema degli scenari previsionali. Se nessun plugin implementa questo hook, XIQUIL usa il motore deterministico integrato.

Il Core legge sempre il plugin.json installato. Nei pacchetti Marketplace questo file viene generato e firmato dall’Hub a partire da due fonti:

  • form Hub per metadata, tier, versione pubblica e compatibilità Core;
  • plugin.source.json per campi tecnici come route, frontend, permessi e pacchetti di sistema.

Durante sviluppo locale puoi usare un plugin.json completo, ma per pubblicare su Marketplace consulta Manifest e metadata plugin.

Schema principale:

CampoTipoNote
namestringObbligatorio, snake_case, pattern ^[a-z][a-z0-9_]*$
versionstringObbligatorio nei pacchetti runtime; per Marketplace viene generato dall’Hub in formato strict x.x.x
display_namestringObbligatorio
descriptionstringObbligatorio
authorstringObbligatorio
required_tierstringDefault pro; valori Hub validi: community, starter, pro, business
min_core_versionstring/nullVersione minima XIQUIL
max_core_versionstring/nullVersione massima XIQUIL
depends_onlist[string]Dipendenze da altri plugin
provides_routesbooleanIl plugin espone API
provides_modelsbooleanIl plugin crea tabelle proprie
provides_frontendbooleanIl plugin include frontend/bundle.js
iconstring/nullNome icona MUI
nav_categorystring/nullCategoria menu
nav_pathstring/nullPercorso frontend
permissionslist[string]Permessi core dichiarati
settings_schemalist[dict]Schema impostazioni plugin
system_packageslist[object]Pacchetti apt richiesti

system_packages usa oggetti con name, optional e description.

enterprise può comparire solo in manifest legacy letti dal Core come alias storico di business; non usarlo nei nuovi plugin e non è accettato dai form Hub.

min_core_version e max_core_version sono controlli bloccanti prima dell’installazione. Usali quando il plugin dipende da API introdotte in una versione specifica del Core o da API che sai essere state rimosse in versioni successive. Nel flusso Marketplace questi valori si impostano nel form Hub e devono usare formato x.x.x.

Le route di router.py sono FastAPI standard.

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}
@router.get(
"/secure-data",
dependencies=[Depends(require_permission("fondi", LIVELLO_SOLA_LETTURA))],
)
def secure_data():
return {"ok": True}

Regole consigliate:

  • proteggi ogni route che legge o modifica dati core;
  • verifica lo scope per ambito quando lavori su fondi, uscite, entrate, scadenze o anagrafiche;
  • usa response model Pydantic/SQLModel dove possibile;
  • non esporre endpoint diagnostici con dati sensibili;
  • tieni /ping leggero per smoke test.

Una route backend del plugin che deve chiamare l’API pubblica del Core puo richiedere la dipendenza CoreApi. La chiamata viene inoltrata nella stessa applicazione con il contesto di autenticazione della richiesta corrente: il Core applica quindi le proprie autorizzazioni e i controlli di ambito.

from fastapi import Depends
from xiquil_plugin_sdk import CoreApi, get_core_api
async def usa_api_core(
core_api: CoreApi = Depends(get_core_api),
):
return await core_api.request_json(
"GET",
"/api/<risorsa>",
params={"chiave": "valore"},
)

request_json() accetta GET, POST, PUT, PATCH e DELETE; per le richieste con corpo usa l’argomento json. Il percorso deve iniziare con /api/ e non puo indirizzare route di plugin, contenere risalite di directory, query o frammenti: passa gli eventuali parametri con params.

La facade stabilizza il trasporto autorizzato, non gli endpoint o i payload del Core: scegli risorse e dati compatibili con la versione Core dichiarata dal manifest. Una risposta 204 restituisce None; gli errori della route Core mantengono stato e dettaglio HTTP. Non importare servizi di mutazione interni del Core per aggirare questa API.

Le tabelle create da plugin dovrebbero usare un prefisso chiaro:

plugin_{nome_plugin}_{risorsa}

Esempio:

plugin_bank_recon_bank_rows
plugin_bank_recon_matches

I plugin non usano Alembic del core. Per aggiornare schema esistente, usa migrazioni idempotenti in on_startup: controlla se colonna o indice esistono, poi applica solo il delta necessario.

Bundle, ABI TypeScript, PluginModule, PluginContext e globali host sono documentati nella reference separata Plugin SDK frontend.

Gli stati principali salvati in database sono:

StatoSignificato
installed_pending_restartInstallato, serve restart
activeInstallato, abilitato e caricato
inactiveInstallato ma disabilitato
update_pending_restartAggiornato, serve restart
uninstall_pending_restartRimozione in attesa

Il tab Sviluppatore nel Marketplace e visibile solo con licenza plugin_developer=true. In sviluppo locale puoi usare PLUGIN_DEVELOPER_MODE=True.

I pacchetti Marketplace devono essere firmati e verificati. L’upload manuale in developer mode puo accettare pacchetti unsigned per test locale.

La verifica controlla:

  • MANIFEST.sha256;
  • MANIFEST.sha256.sig;
  • MANIFEST.sha256.keyid per scegliere la chiave pubblica Marketplace;
  • firma RSA-PSS del manifest;
  • hash SHA-256 dei file elencati.

La chiave pubblica non è hardcoded nel pacchetto: arriva dalla licenza attiva. Il Core usa plugin_pubkeys come trust set per supportare la rotazione delle chiavi Hub; se un pacchetto legacy non contiene MANIFEST.sha256.keyid, usa la chiave attiva plugin_pubkey.

Il catalogo ufficiale mostra solo versioni approvate e firmate dall’Hub. Le versioni beta richiedono una licenza con accesso beta plugin; i plugin di tier superiore possono essere visibili nel Marketplace ma non installabili con una licenza inferiore.

Non usare il developer mode per installare pacchetti di origine non fidata.

  • Un plugin puo importare codice del core, ma deve trattare i contratti core come API interne soggette a cambiamento.
  • Le tabelle plugin devono essere autonome e prefissate.
  • Le migrazioni plugin devono essere idempotenti.
  • Le route devono applicare permission e scope.
  • Il frontend deve usare window.__XQ__ per librerie host.
  • I pacchetti OS vanno dichiarati nel manifest, non installati a mano.
  • Un errore nel plugin viene loggato, ma non deve bloccare il core.