WebEngine bridge, host side
qgis_sdk.bridge is the QGIS-free host half of the transport a web view speaks.
Nothing in it imports Qt or QGIS, and nothing in it takes a callable from the
wire: a view names an operation id, and the host decides whether that view may
have it.
from qgis_sdk.bridge import Dispatcher
runtime = PluginRuntime(MyPlugin, context).start()dispatcher = Dispatcher.from_runtime( runtime, authorize=lambda binding, operation: binding.origin == "qgis://desktop",)
binding, handshake = dispatcher.handshake(view_id="view-1", origin="qgis://desktop")reply = dispatcher.dispatch(binding, request) # never raises-
One manifest, three views.
from_description/from_runtimeproject the allowlist from the plugin’s own declarations, so the operations a view may call, the operations the handshake advertises and the schemas a generated client will be built from are the same object (decision-26). A declared action without a handler is refused when the dispatcher is wired, not when a view calls it. -
Strict inputs, lenient results. A declared request schema is validated before the handler runs - unknown properties, missing required ones, mistyped values and integers outside the safe range are refused as
invalid_payloadwith the field path - and the payload is bound as keyword arguments. A declared result schema is checked afterwards: extra properties are tolerated, a missing or mistyped one isinvalid_resultnaming the field, never the value. -
One narrow entry point. Protocol, generation, authorization, allowlist, payload and serialization are decided in that order. Authorization runs before the allowlist lookup, so a refused view cannot enumerate operations by watching which refusals differ.
-
Trusted identity only.
ViewBindingis minted by the host; a forgedview_idorgenerationinside the payload is ignored, andadvance_generation()retires every live binding withstale_generation. -
Subscriptions.
@app.streamon the application orregister_stream(operation, factory)on the dispatcher declares a stream, anddispatchaccepts{"kind": "subscribe"}. Frames carry a per-subscriptionseq, the queue is bounded (MAX_QUEUED_EVENTS), a pushed stream that overruns it ends with agapterminal carrying the dropped count, progress may be coalesced while it is still queued and data never is, andpump(binding)hands the frames to whatever transport owns the view. -
Release by kind.
{"kind": "release"}disposes handles the way their kind requires: a borrowed layer is never deleted, an owned temporary output is deleted once, and a subscription that already ended reportsalready_releasedinstead of failing. -
Transport.
qgis_sdk.bridge.transport.ChannelTransportmoves JSON strings in and out;qgis_sdk.adapters.webchannel.create_channel_objectwraps it in theQObjectaQWebChannelregisters, importing Qt only when asked.
Generated TypeScript schema module
Section titled “Generated TypeScript schema module”The declarations a plugin already writes are the only source of the page’s
types. One command renders the value types, an Operations and a Streams
interface and the parsed description as web/src/bridge.schema.ts - no QGIS,
no Qt, no handler ever runs - and the page builds its QgisSdk on it
(decision-44;
the TypeScript side is in doc-13):
$ qgis-sdk bridge generate --plugin my_plugin.bridge:appwrote web/src/bridge.schema.ts--out web/src/bridge.ts still renders the 0.3.0 client - validators and
createOperations(client) - for one minor, with a note.
- Deterministic. The same declarations produce the same bytes, so the file is
reviewed and committed like source, and CI regenerates it and fails on any
difference (
--checkreports the drift without writing). - A description works too.
--pluginimportsmodule:Attributefrom the working directory;--description plugin-description.jsondoes the same from a description a host already shipped, for a frontend with no Python at hand. - Closed subset. A schema outside the declaration subset (
$ref, an object inside a union) fails the build instead of emittingany. - Strict in, lenient out. The SDK refuses unknown arguments and accepts a response that gained a field, so a newer host does not break an older view; the generator emits no validator of its own.
- Ids are the method tree.
QgisSdknests methods by the dots in an id (sites.summaryisqgis.sites.summary), so the generator refuses an id that is a prefix of another declared id and one whose first segment is aQgisSdkmember (client,host,schema,ready,close). - Streams come from the same declarations (decision-28).
@app.stream(id=..., events={"data": SiteChange, "progress": ScanProgress})marks a method that returns an iterator ofStreamEvents; its request schema is bound like an action’s, and each declared event kind is a dataclass schema. The runtime registers the stream under its id in the same namespace as the actions, the host advertises the event schemas in the handshake and refuses to emit an event the stream did not declare (the subscription ends witherror), and the page callsqgis.sites.watch(input, handlers)with a typed event union. The frame envelope (seq,gap) is not generated: it is the protocol’s, owned by the TypeScript kernel.
Host surface
Section titled “Host surface”A view can reach the curated services the plugin already implements - and nothing else of the QGIS API. The inventory is a declaration, not a convention:
from qgis_sdk.bridge import Dispatcherfrom qgis_sdk.bridge.host import HostServices, host_grants, register_host_operations
dispatcher = Dispatcher( description=runtime.description, authorize=lambda binding, operation: host_grants()[operation] in grants_for(binding),)register_host_operations( dispatcher, HostServices(layers=layers, project=project, settings=settings, styles=styles, notifications=notifications),)- Six operations, each with a canonical grant.
layers.list,layers.get,project.layer_ids,settings.get,notifications.info,styles.apply. Grants are per operation (layers.read,project.read, …), never per namespace, so a method added later is not automatically allowed. - Everything is declared. Each entry names the Protocol method it wraps, its
guiaffinity and its request/result schema. Every other method of the curated Protocols -open_vector,selection,add_layer,set,execute- is listed inDEFERRED_HOST_METHODSwith the task that owns it, and a test reflects the Protocols so a new method cannot appear unaccounted for. - Nothing unbounded, nothing by path. Every list is paged
(
{limit?, cursor?}in,{items, truncated, next_cursor?}out; 50 by default, 200 at most), a cursor the host never issued is refused rather than silently read as the first page, and no operation takes a filesystem path. - Versioned with the client. The handshake advertises
host_schema_versionand the registered ids, and the TypeScript side ships generated inside the npm package as@archont561/qgis-sdk/host.