Vai al contenuto

Plugin

Un plugin è un pacchetto Python che aggiunge qualcosa a pyarchinit-photogrammetry senza modificarlo: azioni nei menu della finestra (menu Plugin), comandi della riga di comando (ppg plugin NOME), formati di esportazione (File > Esporta), lavori lunghi in background con la barra di avanzamento, impostazioni proprie. I plugin sono raccolti nel repository pyarchinit-photogrammetry-plugins, di cui il programma legge l'indice.

Installare i plugin

  • Nella finestra: Strumenti > Plugin... elenca i plugin installati (attivi, disattivati, o l'errore che ne ha tenuto fuori uno) e quelli del repository dei plugin: Installa / aggiorna, Attiva / disattiva, Rimuovi. Le modifiche valgono dal prossimo avvio (Riavvia ora).
  • Dalla riga di comando:
ppg plugins available          # i plugin dell'indice
ppg plugins install NOME       # nella cartella dei plugin del programma
ppg plugins list               # installati, attivi o no
ppg plugins disable NOME       # conservato, non caricato
ppg plugins remove NOME
ppg plugin                     # i comandi che i plugin mettono a disposizione
ppg plugin COMANDO ARGOMENTI...
  • Con pip, in un ambiente Python: pip install some-ppg-plugin: il programma lo trova dal suo entry point.

Il programma installa i plugin in una cartella propria (~/Library/Application Support/pyarchinit-photogrammetry/plugins su macOS, %LOCALAPPDATA%\pyarchinit-photogrammetry\plugins su Windows, ~/.local/share/... su Linux): l'applicazione impacchettata non ha pip. Un plugin che sta lì vince su uno con lo stesso nome installato con pip. Così si possono installare solo i plugin in puro Python (wheel -py3-none-any), che possono usare i moduli che il programma porta con sé (numpy, SciPy, OpenCV, Open3D, pycolmap, rasterio, shapely, pyproj, SQLAlchemy, PySide6, ...).

I plugin sono codice Python con gli stessi diritti del programma: installa solo quelli di cui ti fidi. PPG_NO_PLUGINS=1 avvia il programma senza nessun plugin (PPG_PLUGINS_ENVIRONMENT=0: solo quelli della cartella dei plugin, nessuno installato con pip); un plugin che fallisce all'avvio viene segnalato (nel log e in Strumenti > Plugin) e lasciato fuori, il programma va avanti.

Iniziare un plugin: ppg plugins new

Il programma scrive il progetto di un plugin da cui partire: un piccolo plugin funzionante con le parti scelte, i suoi test, i README in inglese e in italiano, la nota di licenza, un .gitignore e un workflow GitHub che costruisce la wheel di una release.

ppg plugins new "Conta foto" --dir ~/progetti --author "Your Name" --email you@example.org \
    --description "counts the photos of a chunk" --parts action,background,command,exporter

--parts dice con che cosa parte il plugin, ognuna con un esempio funzionante: action (un'azione nel menu Plugin che mostra un riepilogo del chunk), background (un'azione che legge il DEM in background con la barra di avanzamento), command (ppg plugin conta-foto PROGETTO), exporter (un formato in File > Esporta). La cartella ppg-conta-foto/ contiene il pacchetto ppg_conta_foto con la classe ContaFotoPlugin e l'entry point conta-foto, tests/, README.md, README.it.md, LICENSE, .github/workflows/release.yml. Il comando finisce con i passi successivi: pip install -e . nell'ambiente del programma, pytest -q tests, poi la wheel e la pull request (sotto).

Nella finestra lo stesso si fa da Strumenti > Plugin... > Nuovo plugin...: nome, titolo, descrizione, autore, cartella e le parti da spuntare; la cartella si apre nel gestore dei file appena è scritta.

Scrivere un plugin

Un plugin è un pacchetto con un entry point nel gruppo pyarchinit_photogrammetry.plugins:

ppg-chunk-summary/
    pyproject.toml
    ppg_chunk_summary/__init__.py
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

[project]
name = "ppg-chunk-summary"
version = "0.1.0"
description = "A summary of the chunk: photos, markers, products"
requires-python = ">=3.10"
dependencies = []          # niente che il programma non porti già con sé

[project.entry-points."pyarchinit_photogrammetry.plugins"]
chunk-summary = "ppg_chunk_summary:SummaryPlugin"
from pyarchinit_photogrammetry.plugins import Plugin


class SummaryPlugin(Plugin):
    title = "Chunk summary"
    description = "Photos, aligned photos, markers and products of the chunk"

    def activate(self, context):
        self.context = context
        context.add_action("Riepilogo del chunk", self.show)
        context.add_command("summary", self.command, help="summary of the chunk of a project")
        context.add_exporter("Riepilogo (Markdown)", ".md",
                             lambda chunk, path, progress: path.write_text(self.text(chunk)))

    @staticmethod
    def text(chunk) -> str:
        return (f"# {chunk.label}\n\n- photos: {len(chunk.cameras)}\n"
                f"- aligned: {len(chunk.aligned_cameras)}\n- markers: {len(chunk.markers)}\n")

    def show(self):
        from pyarchinit_photogrammetry.gui.dialogs import message

        chunk = self.context.app.document.chunk
        message(self.context.window, "info", self.title, self.text(chunk))

    def command(self, project):
        import pyarchinit_photogrammetry as ppg

        print(self.text(ppg.Document.open(project).chunk))
        return 0

Mentre sviluppi, pip install -e . nell'ambiente del programma mette in uso il plugin dal prossimo avvio (ppg plugin elenca subito il suo comando).

Che cosa riceve activate

context.
app lo stato condiviso con gli script: context.app.document è il progetto aperto, .chunk il suo chunk attivo (API)
window la finestra principale, o None nella riga di comando
add_action(title, callback, menu="", tip="", processing=False) un'azione di menu sotto Plugin (nel sottomenu del plugin, o in menu); processing=True la disattiva mentre gira un passo dell'elaborazione
add_command(name, function, help="") ppg plugin NOME ARGOMENTI... chiama function(*args) con gli argomenti come stringhe; un int restituito è il codice d'uscita
add_exporter(title, suffix, function) File > Esporta > title...: function(chunk, path, progress) scrive path in background
store_drawings() scrive nel progetto quello che la finestra mostra (unità e quote disegnate, pietre, sezioni) prima che un plugin legga chunk.meta; solo nel thread dell'interfaccia (un'azione, done), niente nella riga di comando; la finestra lo fa da sola prima di eseguire un esportatore
run(title, function, done=None) function(progress) in background con la barra di avanzamento della finestra (subito, nella riga di comando), poi done(result) nel thread dell'interfaccia
data_dir una cartella per i file del plugin
settings, save_settings() un dizionario conservato da una sessione all'altra
log un logger: i suoi messaggi vanno nel log del programma

progress(fraction, message) riporta un valore tra 0 e 1 e solleva Cancelled quando l'utente annulla: chiamala spesso nei cicli lunghi. I widget si toccano solo nelle callback delle azioni e in done, mai nella funzione data a run (gira in un altro thread). deactivate() rilascia quello che activate ha preso oltre alle sue registrazioni (file aperti, timer).

Compatibilità

Plugin.api è l'API dei plugin per cui il plugin è stato scritto (pyarchinit_photogrammetry.plugins.API_VERSION, ora 1): un programma con un'API più vecchia rifiuta il plugin con un messaggio. L'indice dà anche la versione più vecchia del programma con cui un plugin funziona (min_app).

Pubblicare un plugin

  1. Costruisci una wheel in puro Python: python -m build --wheel (dist/ppg_chunk_summary-0.1.0-py3-none-any.whl).
  2. Allegala a una release del tuo repository (o pubblicala su PyPI).
  3. Apri una pull request sul repository dei plugin aggiungendo la tua voce a index.json:
{
  "name": "chunk-summary",
  "title": "Chunk summary",
  "description": "Photos, aligned photos, markers and products of the chunk",
  "i18n": {"it": {"title": "Riepilogo del chunk", "description": "Foto, foto allineate, marker e prodotti del chunk"}},
  "version": "0.1.0",
  "author": "Your Name",
  "homepage": "https://github.com/you/ppg-chunk-summary",
  "license": "GPL-2.0-or-later",
  "api": 1,
  "min_app": "1.1.0",
  "requires": [],
  "wheel": "https://github.com/you/ppg-chunk-summary/releases/download/v0.1.0/ppg_chunk_summary-0.1.0-py3-none-any.whl",
  "sha256": "the output of: shasum -a 256 ppg_chunk_summary-0.1.0-py3-none-any.whl"
}

name è il nome dell'entry point, requires elenca i moduli che il plugin importa oltre a quelli del programma, i18n dà titolo e descrizione in altre lingue (mostrati quando l'interfaccia le parla). python scripts/make_entry.py WHEEL_URL --write nel repository dei plugin crea la voce dalla wheel. Il repository controlla la voce quando la pull request viene aperta: l'hash, l'entry point, e che il plugin parta con il programma. Solo il manutentore fa il merge: quella è l'approvazione. Un plugin può anche stare nel repository dei plugin stesso, sotto plugins/ (vedi il suo README); template/, lì, è il plugin da cui parte ppg plugins new.