Processing algorithms and safe outputs
qgis_sdk.algorithms is the authoring surface
decision-42
settled on: an algorithm is a plain function declared on a Provider, the
same shape as a @job. Its first parameter is the AlgorithmContext the
runtime injects, the remaining parameters are typed keyword inputs, and it
returns the VectorLayerInfo of the layer it produced. Describing one never
calls it, and every invocation builds a fresh context over a child scope
that is closed again when the invocation ends:
from qgis_sdk.algorithms import describe_algorithm, invokefrom qgis_sdk.core import AlgorithmContextfrom qgis_sdk.models import BufferRequest, CrsInput, LayerInput, Metres, VectorLayerInfofrom qgis_sdk.providers import Provider
tools = Provider(id="field_tools", title="Field tools")
@tools.algorithm(title="Buffer in a projected CRS", group="Geometry")def buffer( ctx: AlgorithmContext, layer_id: LayerInput, distance: Metres, crs: CrsInput) -> VectorLayerInfo: return ctx.algorithms.execute( BufferRequest(layer_id=layer_id, distance_metres=distance, working_crs=crs) )
described = describe_algorithm(buffer)print(described.qualified_id, described.kinds, described.required)# field_tools:buffer {'layer_id': 'layer', 'distance': 'metres', 'crs': 'crs'} ('layer_id', 'distance', 'crs')How do I run one from a context?
Section titled “How do I run one from a context?”invoke(function, ctx, **inputs) runs one from the context the caller already
holds - the PluginContext of an action or the AlgorithmContext of another
algorithm. A @job body cannot: it runs on a worker, so its WorkerContext
carries neither algorithms nor project (decision-65) - invoke the algorithm
from the subscriber that hears the job succeed. It reads algorithms, project
and scope from that context, builds the invocation’s own AlgorithmContext,
and adds the output to ctx.project after success:
# inside an @app.action functionoutput = invoke( buffer, ctx, layer_id=source_id, distance=500.0, crs="EPSG:3857")# invoke already added `output` to the project - after success only.progress= and cancellation= left out are inherited from a job or algorithm
context (so a job’s cancellation reaches the algorithm it runs); a plugin
context has neither, and the call runs silently and uncancelled.
add_to_project=False is for a caller that owns the destination itself: the
Processing bridge hands the layer to a feature sink, and the reference workflow
styles the output before presenting it. A context without the service a call
needs refuses by name (QgisSdkUnsupportedCapabilityError("algorithms"))
before anything runs.
No execution context is injected at decoration time, so a unit test needs no runner: call
buffer(fake_context, layer_id=..., distance=..., crs=...) with an
AlgorithmContext built over the qgis_sdk.testing fakes.
What does the description carry?
Section titled “What does the description carry?”The input schemas come from the same qgis_sdk.declarations generator that
describes plugin actions, so an algorithm’s inputs are held to one documented
typing subset. AlgorithmDescription carries parameters, required,
annotations, kinds (input name to host parameter kind), inputs_schema,
result_type / result_schema, group, provider_id and qualified_id;
to_dict()["inputs"] is the object schema itself.
Safe outputs
Section titled “Safe outputs”Non-destructive by construction (decision-19)
Section titled “Non-destructive by construction (decision-19)”Algorithms.execute directs the built-in buffer and centroid operations to a
new SDK-owned output, then opens it as a layer. The SDK attempts to remove its
run directory on failure and its intermediate buffer reprojection on success;
filesystem cleanup can itself fail. The reference native algorithms are tested
for source preservation, but Processing providers are trusted code, not a
sandbox: the SDK cannot prevent a provider from editing inputs or writing
elsewhere, or roll back those side effects. invoke adds the returned output to
the project only after success, type validation and a cancellation check; an
algorithm or provider may still have its own project side effects.
Explicit CRS and units where a distance is involved
Section titled “Explicit CRS and units where a distance is involved”BufferRequest requires working_crs and distance_metres; the native
backend refuses a working CRS that is geographic (a distance there would be
degrees) or whose units are not metres, instead of converting behind the
caller’s back. CentroidRequest has no distance and therefore no working CRS:
decision-19 requires a projected CRS where the operation has one, not
everywhere.
Two honest limits of this slice: cancellation is observed at phase boundaries (before the function runs, and after it before the project is touched), not by aborting a native operation mid-flight; and an algorithm’s result is a vector layer - other layer kinds arrive with the services that produce them.
Any registered algorithm: list, describe, run and the generated registry
Section titled “Any registered algorithm: list, describe, run and the generated registry”BufferRequest and CentroidRequest are the hand-written predecessors of a
generated family. Since 0.7.0
(decision-64)
ctx.algorithms reaches every algorithm the host’s Processing registry
holds - native, qgis, gdal, 3d, pdal, GRASS, SAGA, a third-party plugin’s -
through one surface:
from qgis_sdk.processing import native
providers = ctx.algorithms.providers() # ProviderInfo per registered providersummaries = ctx.algorithms.list(provider="gdal") # AlgorithmInfo, help and allinfo = ctx.algorithms.describe("native:buffer") # parameters, outputs, defaults
# Three spellings of one run; all return a RunResult.result = ctx.algorithms.run(native.Buffer(input=layer.id, distance=25.0))result = ctx.algorithms.run("native:buffer", {"INPUT": layer.id, "DISTANCE": 25.0})result = ctx.algorithms.run(AlgorithmRun("native:buffer", {"INPUT": layer.id}))
buffered = result.vector("OUTPUT") # an owned VectorLayerInfostats = ctx.algorithms.run( "native:basicstatisticsforfields", {"INPUT_LAYER": layer.id, "FIELD_NAME": "id"})stats.value("COUNT") # 3report = stats.file("OUTPUT_HTML_FILE") # a read-only FileRef; ctx.files.resolve(report)The request is validated against the algorithm’s description before execution
(QgisSdkAlgorithmRejectedError describes a refusal; an unknown id is
QgisSdkAlgorithmNotFoundError). A layer input is an owned layer id, a
FileRef, or a path the plugin already holds. A destination left out is
allocated under a fresh SDK-owned run directory: vector sinks return as
VectorLayerInfo, files and folders as read-only FileRef objects, and scalar
outputs as values.
Named destination safety (unreleased change). A run may name at most one
new local GeoJSON or GeoPackage vector destination, as a path string or a
write-authorized FileRef. Existing files, including input aliases and
write-granted files with overwrite permission, are refused; so are multiple,
remote, memory, folder, raster and other named destinations. The native adapter
writes to a private directory beside the destination, verifies a single readable
independent file with no sidecars, then atomically publishes it without replacing
an existing name. This requires hard-link support; a FileRef write grant is
rechecked before publication. Leave a destination unspecified and export the
resulting vector with ctx.layers.export_vector when you need
explicit overwrite consent; for other output types use SDK-allocated output and
handle publication separately with suitable safeguards.
Cleanup of SDK-owned run and staging directories is best-effort. The SDK never
intentionally deletes an existing destination, but a complete file already
published may remain if later result conversion fails. Keep paths stable during a
run. Processing providers are trusted code and can modify inputs, project state
or other files outside the SDK’s directories; the SDK cannot prevent or roll
back such side effects. The fake uses scripted answers and validates destination
requests but does not run Processing or prove filesystem behavior. run is
synchronous on the GUI thread, like processing.run; a long algorithm belongs
in a @job.
The generated modules
Section titled “The generated modules”qgis_sdk.processing.native, .qgis, .gdal, .p3d and .pdal are
generated, never edited: each carries a TypedDict per algorithm (the
shape run(id, params) is typed by), a request class per algorithm
(native.Buffer, native.Centroids, … - frozen dataclasses whose fields
are the parameters in snake case, destinations defaulting to None) and a
REGISTRY of AlgorithmInfo. They come from a snapshot of the registry of
the QGIS the SDK release was qualified with
(qgis_sdk/processing/catalogue.json; QGIS_VERSION says which), kept
fresh by scripts/generate_processing_snapshot.py and a CI drift check. A
host whose registry differs is what describe reports; the host’s
description, not the snapshot, is what a run is validated against.
Over the fakes
Section titled “Over the fakes”Over the fakes, FakeAlgorithms.list/describe answer from that snapshot
(or a Catalogue the test passes as catalogue=), and run answers from
what the test scripted with script_run(id, outputs) - a mapping of output
name to a Path (becomes the registered layer or a FileRef, by the
output’s kind) or a scalar, or a callable of the AlgorithmRun - and
records every run in .runs. A run nobody scripted fails with
QgisSdkAlgorithmFailedError, never with an invented layer.
Generating modules for another provider
Section titled “Generating modules for another provider”A project that depends on a provider the runtime does not ship generates its own modules with the developer tools:
qgis-sdk processing generate --provider grass --provider saga # reads the project's QGISqgis-sdk processing generate --check # CI: fail on driftThe reader runs inside the project’s QGIS interpreter (the tools never
import QGIS); the output is <plugin package>/processing/ in Python
(--out) and web/src/processing.schema.ts for the page (--web-out),
each skippable (--no-python, --no-web). Pass --catalogue <json> to
render from a catalogue a reader already wrote.