Architecture¶
How the plugin is put together, and the helpers every tab goes through. Read
this page and the invariants before you change code: they
record what the code cannot tell you. Python 3.12 (QGIS 3.40 and newer),
PyQt5 and PyQt6 through qgis.PyQt.
Layout¶
Path |
What lives there |
|---|---|
|
QGIS entry point: |
|
|
|
One mixin per resource type (the Server tab: one row per server-wide setting, no Add or Delete): load / add / edit / delete (layers also: publish, add to QGIS, preview, set styles, push a style from QGIS, update from the data; layer groups also: add to QGIS, preview; datastores and coverage stores also: reset; coverage stores also: publish a coverage; cascaded stores also: publish / view a remote layer; styles also: apply to a QGIS layer, save to disk, copy, used by; tile cache: configure, seed and follow the tasks, truncate, stop caching) |
|
|
|
|
|
|
|
Options page: URL, user name and password (plain text in QgsSettings by default; a box per connection keeps them in |
|
|
|
|
|
Bundled |
|
Runs without QGIS. |
|
The site: Sphinx + MyST + Furo, deployed to GitHub Pages on every push to main. |
|
|
|
Throwaway GeoServer 2.28.5 ( |
How the dialog works¶
Tabs are one registry.
GeoServerMainDialog.TABS = ((label, icon, loader_name), …)._setup_navbuilds the list from it and_on_nav_changedcallsgetattr(self, loader)(). A new resource type is one line inTABSplus one mixin added to the class bases.A loader sets up the header buttons,
_name_click_callback,_extra_click_callbacksand_row_actions, calls_setup_table(columns), then hands a fetch function to_start_load(failure_message, fetch)and returns. Rows are plain lists of display strings; column 0 is the resource name. A_row_actionsentry is(icon, label, callback), plus an optional fourth element when the action’s tooltip must say more than the label (the browser preview’s login note)._make_action_widgetkeeps up to two frequent actions visible (add to QGIS, preview, browse and publish). The other actions get labels in a More or Actions menu, with the destructive actions last and separated when needed. Both paths check the connection at activation and capture the row from the page render. Keep this grouping central; a tab only declares its own action tuples.Loads run off the GUI thread.
_start_loadwraps the fetch in a_FetchTask(aQgsTask), so_load_x()returns before a single row exists. QGIS’s task bar shows the progress, Refresh turns into Cancel, andfinished()comes back on the GUI thread to render. A fetch is_fetch_<x>_rows(task=None) -> (rows, failures). It runs in a worker, so it must not touch a widget, and it only reaches the task by passing it to_fan_out. A read’s worker is a thread of its own, and a cancel lets go of it at once. So a hung request holds neither the task nor QGIS’s exit, which cancels a read without asking (CancelWithoutPrompt). An upload and a delete batch are waited for, and QGIS asks first.A fetch lists names only. A column that needs one GET per row holds
scope.PENDING. After_setup_table, the loader setsself._row_detail = row -> cells(a stateless read, run in a worker) andself._detail_columns._show_pagethen fetches the pending cells of the 20 rows on screen, in the_detailslot. It writes them into the shared row lists in place. A row action, and a sort on a detail column, first complete the rows they need (_complete_rows, from_addressableand_on_header_clicked). A late fill checks_table_generation, so it never lands in another tab’s table. The search box matches the cells loaded so far. Measured: the Styles tab with 147 styles went from 158 requests to 31.Mutations (add / edit) run under
_run_action, with their requests in_wait_for. The user is waiting for the dialog they confirmed, but a hung server must not freeze QGIS for the library’s 120 s timeout. So the action passed to_wait_formakes requests only. A question (_push_qgis_style’s “Replace the style?”) or a warning stays outside it. A helper that runs in it returns its warning instead of showing it (_save_workspace). Deletes run in the_deletetask slot.An upload is the exception (
_run_upload). Its body is atoolbelt.rest.ProgressReader, which moves the task bar from eachread()and raises on Cancel, sorequestsdrops the connection mid-body. The work gets the REST client as an argument, because a Refresh clearsself.gswhile it runs (toolbelt.rest.raw_restis_raw_restfor a client you hold).The table is paginated in Python (
_page_size = 20,_all_rows→_filtered_rows→ one page)._get_selected_rows()maps a selected view row back through_filtered_rowsby index.Server calls go through the helpers on the dialog, never hand-rolled in a mixin:
Helper
Use it for
_run_action(fn, failure_message) -> boolany mutation: wait cursor, banner + QGIS log on failure
_fetch(fn, failure_message) -> value | Noneany read the UI needs before continuing: runs
fnin a worker thread and waits behind an application-modal Waiting for GeoServer box (after 0.3 s) with Cancel, so a dead server cannot freeze QGIS.in_worker=Falsefor work on a live QGIS layer. A map layerfnbuilds comes back moved to the GUI thread_wait_for(fn) -> valuethe same wait without the reporting: a read inside a handler that does its own (the Publish form’s combo refills).
fnmust not touch a widget, nor call_wait_foror_fetchitself_form_check(check) -> validatea form’s
validate:check(values)behind the waiting box, on Save, before the form closes, so a refusal (a taken name) keeps what was typed.checkonly reads (_check_new_datastore,_check_new_workspace,_check_group_rows…); the save runs after the form closed and checks again. A pure check (a tile cache edit) is passed asvalidatedirectly_wait_for_save(fn) -> valuea save’s requests under
_run_action. A Cancel cannot stop a request already sent: the save still lands, so the banner says it may, and the tab reloads once it ends. Until then a task, GeoServer Manager: saving…, keeps QGIS from quitting before it ends_check((content, status))unwrap a geoservercloud tuple; raises on ≥ 400
_fetch_list(api_method, *args)a list endpoint; raises when the payload is not a list (a sign-in page), which must not render as an empty table
_resource_exists(getter, *args)pre-check before Add (the library upserts)
_raw_rest(method, path, **kw)endpoints the library lacks; raises with GeoServer’s response body, except for a status in
accept=(404,), which comes back as the answer it is (a settings path with none of its own)_name_of(item)the name of a list entry (dict or str)
_get_workspace_names()workspace names for combos: a fresh GET every call, deliberately uncached
_start_load(failure_message, fetch)a tab load: runs
fetch(task)in aQgsTask, renders(rows, failures)when it lands_run_in_task(failure_message, work, on_success, on_cancel=…, busy_text=…)the same for anything that is not rows (the connection probe); a load supersedes it. Deletes run in their own
_deleteslot through_delete_many_run_quietly(failure_message, work, on_success)a side fetch (a dialog’s legend) in its own slot: never supersedes a load, never turns Refresh into Cancel
_run_upload(failure_message, work, on_success, on_cancel)a long PUT: streams in its own task slot (
_upload) with progress and Cancel; a load never supersedes it and it never touches the table:on_successreloads through_reload_current_tab(),on_cancelsays what the server kept_upload_file(failure_message, client, url, source, params, headers, on_success, on_cancel, folder=…, after=…, on_done=…)the one way a file leaves this machine: streams
sourcethrough_run_upload, removesfolderhowever it ends, runsafter(client)in the worker for a follow-up PUT. Check_upload_slot_free()before exporting._report_cancelled_upload(kind, tab, exists, name)is itson_cancel.on_done(outcome)runs once at the end with “done”, “failed” or “cancelled”: the batch publish (_publish_layers) starts its next layer from it, since only one upload runs at a time. Returns False when nothing started_cancel_load(user=False)stop the running load;
user=Trueis the Cancel button, which lets go of the task at once (a hung request only returns at its timeout) and explains itself in a banner. A load cancelled from QGIS’s task bar says so too; only a superseded one stays quiet_fan_out(fn, items, task=None) -> [(result, error)]parallel per-item GETs, eight at a time for the whole plugin (so
fnnever fans out itself); a failing item yields(None, exc)instead of aborting. With the task: progress per finished item, and once it is cancelled no item starts: it yields(None, CancelledError())_scoped_names(list_global, list_in, workspace_names, task=None) -> (pairs, failures)the listing of a resource that is global or per workspace (styles, layer groups):
(name, workspace label)pairs, the global ones first fromlist_global(), onelist_in(ws)per workspace fanned out, a listing that fails (the global one too) reported beside the names_report_partial_failures([(label, exc)])one warning banner + log lines for what a listing could not fetch
_delete_many([(label, fn)], reload_fn, ask, done, cascade=…)confirm + run in a task of its own slot (
_delete), with progress, and report one or many deletions. A tab switch or F5 never stops it, Cancel does, one batch runs at a time, and it reloads only the tab it started from.askanddoneare whole sentences from_one_or_many(one, many):one.format(name)for a single resource, elsemany(count), the tab’sn -> translate(ctx, "…%n layer(s)…", None, n), so each locale gets its plural forms (translations); a tab whose action is not a delete (the tile cache’s “stop caching”) words them that way_require_safe_name(name)every Add form, before any request: refuses
/ ? # %and edge spaces;requestssendsdatastores/a#b.jsonasdatastores/a_yes_no(value)a boolean cell, translated, never Python’s
True/False_unwrap/_as_list/_name_ofGeoServer’s collection shapes, from
toolbelt/payload.py; no tab keeps its own copy_reload_current_tab()after an action reachable from another tab
_wire_picker(dlg, picker, first, load)a read-only viewer with a picker (a store’s coverages, its cascaded layers): choosing an entry fills the form through
ResourceFormDialog.set_values;loadreturns None after a failed read, which leaves the fields aloneAdding a layer to QGIS (
LayerTabMixin._add_layer_to_qgis): build the URI with_layer_uri(pure, tested), constructQgsRasterLayerorQgsVectorLayer, checkisValid(), thenQgsProject.instance().addMapLayer(). Neveriface.addRasterLayer(): on failure it pops QGIS’s own modal instead of our banner. An encrypted user name and password travel asauthcfg=<geoserver_auth_cfg_id>, which the providers resolve fromQgsAuthManager, so a saved project contains no password. A plain one (the default) goes in the source asusernameandpassword, which a saved project then holds. The plugin’s TLS setting does not reach QGIS’s providers; they use QGIS’s own certificate handling.
The server and the library¶
They are on their own page, GeoServer and library notes. You need them when you touch one tab’s server calls, not for every change. Read it before you write or change any GeoServer call. What is in it:
Which verbs raise, which return
(content, status), and why_checkexists.Which library methods upsert, which are missing, and what goes through
_raw_rest.Per resource type: layers of every type, legends and previews, coverages, cascaded WMS and WMTS stores, layer groups, workspace WMS settings. Also the tile cache and its XML-only writes, and file-based and cascaded WFS datastores.
Publishing from QGIS: what a GeoPackage or GeoTIFF upload creates, what a cancelled upload leaves behind, and how SLD versions pick a content type.
The bundled wheel: why it is stripped, and what to do on a version bump.