stratified_packager.processing.project_builder

Embedded per-stratum project construction (SPEC §13).

Runs on the algorithm thread during Phase C — never against instance(). The fresh project re-points included layers at the stratum GeoPackage tables and the data/ payload copies, restores the layer-tree structure (groups, order) and each node’s presentation state (check state, expanded state, legend customizations) restricted to included layers, applies the full (rewritten) styles, remaps relations among included layers, and carries the project CRS, transform context, title and the source’s initial map view (so it opens at the same position and zoom). Paths are stored relative: the caller builds the stratum inside a directory tree that mirrors the zip layout, so Qt’s relative-path storage produces portable ./… sources (SPEC §13).

Functions

build_stratum_project(source, plan, feedback)

Build and write one stratum's embedded project (SPEC §13).

index_join_columns(source, plan, feedback)

Index the columns this stratum's re-pointed layers join on, in its gpkg (SPEC §13).

read_saved_view_extent(project_file)

Read the last-saved map-canvas extent from a project file.

resolve_initial_view(project)

Resolve where the source project opens, for the embedded projects (§13).

snapshot_embedded_layers(layers)

Serialize the embedded-only layers once for the whole run (SPEC §13).

validate_repointed_sources(layers, tables, /)

Check the §4 project_only layers can be re-pointed at the stratum gpkg (§13).

Classes

StratumProjectPlan(title, mode, gpkg_path[, ...])

Everything needed to build one stratum's embedded project.

class stratified_packager.processing.project_builder.StratumProjectPlan(title, mode, gpkg_path, qgz_path=None, vector_tables=<factory>, data_sources=<factory>, embedded_only=(), repointed=frozenset({}), embedded_xml=<factory>, styles_qml=<factory>, subsets=<factory>, display_names=<factory>, initial_view=None)[source]

Everything needed to build one stratum’s embedded project.

Parameters:
  • title (str)

  • mode (ProjectInclusion)

  • gpkg_path (Path)

  • qgz_path (Path | None)

  • vector_tables (dict[str, str])

  • data_sources (dict[str, Path])

  • embedded_only (tuple[str, …])

  • repointed (frozenset[str])

  • embedded_xml (dict[str, str])

  • styles_qml (dict[str, str])

  • subsets (dict[str, str])

  • display_names (dict[str, str])

  • initial_view (QgsReferencedRectangle | None)

__init__(title, mode, gpkg_path, qgz_path=None, vector_tables=<factory>, data_sources=<factory>, embedded_only=(), repointed=frozenset({}), embedded_xml=<factory>, styles_qml=<factory>, subsets=<factory>, display_names=<factory>, initial_view=None)
Parameters:
  • title (str)

  • mode (ProjectInclusion)

  • gpkg_path (Path)

  • qgz_path (Path | None)

  • vector_tables (dict[str, str])

  • data_sources (dict[str, Path])

  • embedded_only (tuple[str, …])

  • repointed (frozenset[str])

  • embedded_xml (dict[str, str])

  • styles_qml (dict[str, str])

  • subsets (dict[str, str])

  • display_names (dict[str, str])

  • initial_view (QgsReferencedRectangle | None)

data_sources: dict[str, Path]

Included payload layers: source layer id -> absolute path of the data/ copy inside the zip-mirror tree.

display_names: dict[str, str]

Source layer id -> custom display name for this stratum (SPEC §4 layer_name, already evaluated). Absent ids keep the original layer name.

embedded_only: tuple[str, ...] = ()

Source layer ids riding only in the project (remote sources, annotations).

embedded_xml: dict[str, str]

The run’s snapshot_embedded_layers() result: source layer id -> serialized <maplayer> document. Shared by every stratum, so a remote layer is reproduced without reopening its source. Absent ids fall back to clone().

gpkg_path: Path

The stratum GeoPackage (absolute, already built, inside the zip-mirror tree).

initial_view: QgsReferencedRectangle | None = None

The source project’s initial map view (§13) — its saved canvas extent, else its configured default view extent — applied to the embedded project’s view settings so it opens at the same position and zoom. None leaves QGIS’s full-extent default.

mode: ProjectInclusion

gpkg (project storage inside the GeoPackage) or qgz (file beside it).

qgz_path: Path | None = None

The .qgz destination for qgz mode (beside the gpkg, same basename).

repointed: frozenset[str] = frozenset({})

Source layer ids marked matching_method = project_only (§4) — a subset of embedded_only whose data source is re-pointed at this stratum’s gpkg (§13) instead of carried over unchanged.

styles_qml: dict[str, str]

Source layer id -> rewritten QML document (SPEC §14 asset paths).

subsets: dict[str, str]

Source layer id -> subset string to re-apply (SPEC §12/§13).

title: str

Project title (the stratum name).

vector_tables: dict[str, str]

Included vector layers: source layer id -> table present in the stratum gpkg.

stratified_packager.processing.project_builder._append_unplaced(fresh, fresh_root, replacements, placed)[source]

Append included layers that had no source tree node (legend-less additions).

Parameters:
Return type:

None

stratified_packager.processing.project_builder._apply_initial_view(fresh, plan)[source]

Set the embedded project’s initial map view from the plan (§13).

A headless write() emits no <mapcanvas> element, so the written project has no per-canvas saved extent; QgsProjectViewSettings’ default view extent is therefore honoured on open, landing the canvas on the source project’s view instead of the full layer extent.

Parameters:
Return type:

None

stratified_packager.processing.project_builder._apply_styles_and_subsets(plan, replacements, feedback)[source]

Apply the rewritten QML styles and re-apply subset strings.

Parameters:
Return type:

None

stratified_packager.processing.project_builder._build_layers(source, plan, feedback)[source]

Create the fresh project’s layers, keyed by their source layer id.

Broken layers are never included (SPEC §13 bad-layer policy) — a re-pointed layer that fails to open is dropped with a warning.

Parameters:
Return type:

tuple[dict[str, QgsMapLayer], list[tuple[float, str]]]

Returns:

Source layer id -> replacement layer, and one (seconds, name) pair per layer opened, for the caller’s timing line.

stratified_packager.processing.project_builder._capture_log()[source]

Collect QgsMessageLog entries emitted on the current thread.

Surfaces why write() failed: it returns only a bool while the cause is logged. The write runs synchronously on the algorithm thread, so its emissions invoke the handler before the block exits. The handler is a plain callable with no receiver thread affinity, so an unrelated emission from another thread during this window would run it on that thread — a tolerated, GIL-safe append of a stray line.

Yield:

The list each entry ([tag] message) is appended to.

stratified_packager.processing.project_builder._copy_node_state(source_node, target_node)[source]

Mirror one layer-tree node’s presentation state onto its replacement (SPEC §13).

Carries the collapsed/expanded state, the check state, and every custom property the node holds. That property bag is where QGIS keeps the rest of what the Layers panel shows — the legend feature counts (showFeatureCount) and the legend-node customizations (renamed, reordered and hidden classes, under legend/…) — so the packaged project’s panel reads like the original’s. It is copied wholesale rather than key by key so this does not drift as QGIS grows new node state; only the keys that would point QGIS back at the source machine are dropped (_TREE_NODE_SKIP_PROPERTIES).

Parameters:
  • source_node (QgsLayerTreeNode) – The node in the project being packaged.

  • target_node (QgsLayerTreeNode) – The freshly created node in the embedded project.

Return type:

None

stratified_packager.processing.project_builder._embedded_replacement(original, source, plan, feedback)[source]

Build the embedded-only replacement for one layer.

A §4 project_only layer keeps its uri options and only swaps the file path for this stratum’s gpkg (_repoint_layer_source()); a live virtual layer is re-pointed source by source (_rebuild_virtual_layer()). Everything else is reproduced from the run’s snapshot_embedded_layers() XML, which keeps the original source without reopening it; only a layer type the snapshot cannot reproduce falls back to clone().

Parameters:
  • original (QgsMapLayer) – The source project’s embedded-only layer.

  • source (QgsProject) – The open project being packaged.

  • plan (StratumProjectPlan) – The stratum’s project plan.

  • feedback (QgsProcessingFeedback) – Execution feedback channel.

Return type:

QgsMapLayer | None

Returns:

The replacement layer, or None to drop it.

stratified_packager.processing.project_builder._fast_open()[source]

Build the layer options of a re-pointed embedded-project layer.

loadDefaultStyle=False: the style is applied explicitly afterwards from the exported QML (the very payload the gpkg layer_styles rows carry — _finalize_layer writes both from the same source), so the per-layer default-style lookup is a redundant read. skipCrsValidation=True: the CRS comes from the just-written gpkg (or the virtual query); there is nothing to interactively validate on a worker thread.

Return type:

LayerOptions

Returns:

The options for QgsVectorLayer construction.

stratified_packager.processing.project_builder._index_source_table(gpkg_path, table, candidates, layer_name, feedback)[source]

Index the candidates that are real columns of table (SPEC §13).

The intersection with the table’s own columns is what keeps equality_operands() a scan instead of a parser: a literal or alias it also matched has no column here and is dropped.

Parameters:
  • gpkg_path (Path) – The stratum GeoPackage.

  • table (str) – A table the querying layer reads.

  • candidates (frozenset[str]) – Identifiers the querying layer compares with =.

  • layer_name (str) – The querying layer’s name, for messages.

  • feedback (QgsProcessingFeedback) – Execution feedback channel.

Return type:

None

stratified_packager.processing.project_builder._primary_map_canvas(document)[source]

Find the primary <mapcanvas> element, preferring the theMapCanvas main view.

Parameters:

document (QDomDocument) – The parsed project XML.

Return type:

QDomElement | None

Returns:

The chosen element, or None when the project has no <mapcanvas>.

stratified_packager.processing.project_builder._read_project_document(project_file)[source]

Load a project file’s XML — .qgs directly, the .qgz archive’s inner .qgs.

Parameters:

project_file (Path) – The source project file.

Return type:

QDomDocument | None

Returns:

The parsed document, or None when the file is missing, is not a .qgs/.qgz, or does not parse as XML.

stratified_packager.processing.project_builder._rebuild_virtual_layer(original, source, plan, feedback)[source]

Re-point a live virtual layer’s sources at this stratum’s gpkg tables.

Each source the virtual layer queries is rewritten to the GeoPackage table that holds that layer in this stratum; the query, subset, uid and geometry definition are preserved, and the layer is left without a computed extent so the build never runs the query. The layer is dropped (returning None) when any source has no table in this stratum (e.g. an empty layer omitted under KEEP_EMPTY_LAYERS=False). Style and attribute-form config ride along by cloning the original and only swapping its data source.

Parameters:
  • original (QgsMapLayer) – The source project’s virtual layer.

  • source (QgsProject) – The open project being packaged (resolves source references).

  • plan (StratumProjectPlan) – The stratum’s project plan.

  • feedback (QgsProcessingFeedback) – Execution feedback channel.

Return type:

QgsMapLayer | None

Returns:

The re-pointed virtual layer, or None to drop it.

stratified_packager.processing.project_builder._remap_relations(source, fresh, replacements, feedback)[source]

Recreate the relations whose both ends are included.

Relations touching excluded layers are dropped.

Parameters:
Return type:

None

stratified_packager.processing.project_builder._rename_layer_element(document, element, name)[source]

Rewrite a serialized layer’s <layername> in place.

Parameters:
  • document (QDomDocument) – The document owning element (creates the node when absent).

  • element (QDomElement) – The <maplayer> element.

  • name (str) – The display name to store.

Return type:

None

stratified_packager.processing.project_builder._replicate_tree(source, fresh, replacements, feedback)[source]

Replicate the layer tree (groups, order, node state) for included layers.

Parameters:
Return type:

None

stratified_packager.processing.project_builder._repoint_layer_source(original, plan, feedback)[source]

Re-point a §4 project_only layer’s data source at this stratum’s gpkg (§13).

Only the file path changes: every uri option after it rides along verbatim — including a |subset= holding a whole SELECT, which is the layer’s entire definition. Such a layer is authored against the package, naming the tables and columns the run writes, so rewriting anything else would break it. Nothing here reads the original, which is the point: its own source need not exist on the packaging machine.

Parameters:
  • original (QgsMapLayer) – The source project’s project-only layer.

  • plan (StratumProjectPlan) – The stratum’s project plan.

  • feedback (QgsProcessingFeedback) – Execution feedback channel.

Return type:

QgsMapLayer | None

Returns:

The re-pointed layer, or None to drop it.

stratified_packager.processing.project_builder._report_slowest_steps(title, elapsed, feedback)[source]

Push one line breaking this stratum’s embedded-project build down by step.

Read against the caller’s total for the whole phase: when the two agree the cost is in a layer this line names, and when they diverge it is in the tree/style/relation work between them. That pairing is what makes a stall in here attributable at all — see slowest_summary().

Parameters:
Return type:

None

stratified_packager.processing.project_builder._resolve_virtual_source_table(src, source, plan)[source]

Find the stratum gpkg table backing one virtual-layer source.

References by layer id resolve directly; embedded sources match the first packaged layer with the same provider and source string.

Parameters:
Return type:

str | None

Returns:

The table name, or None when the source is not packaged here.

stratified_packager.processing.project_builder._revive_embedded_layer(original, xml, plan)[source]

Rebuild one embedded-only layer from its snapshot XML, without opening its source.

FlagDontResolveLayers fills the layer’s state from the document but skips provider construction, so no network request is made. The layer therefore has no provider to serialize itself back from, which is what setOriginalXmlProperties() covers: write() emits those bytes verbatim. That also makes the stored document — not the layer object — authoritative, so a §4 layer_name override has to be patched into the XML rather than applied with setName.

Parameters:
  • original (QgsMapLayer) – The source project’s layer (its type picks the empty layer to fill).

  • xml (str) – The layer’s snapshot document.

  • plan (StratumProjectPlan) – The stratum’s project plan.

Return type:

QgsMapLayer | None

Returns:

The replacement layer, or None for the caller to fall back to cloning.

stratified_packager.processing.project_builder._source_options(layer)[source]

Return layer’s uri options: everything from the first | of its data source on.

Read from the stored uri text rather than from the provider, so it answers for a layer whose source does not exist on this machine — the normal state of a §4 project_only layer, and the reason it is never opened.

Parameters:

layer (QgsMapLayer) – The layer whose data source is inspected.

Return type:

str

Returns:

|layername=…|subset=… and friends, leading separator included; empty when the uri carries no options.

stratified_packager.processing.project_builder._write_failure_detail(plan, messages)[source]

Describe why write() failed, for the raised exception.

Prefers the captured log; falls back to filesystem facts about the destination when the log stayed silent.

Parameters:
Return type:

str

Returns:

A single-line detail string.

stratified_packager.processing.project_builder.build_stratum_project(source, plan, feedback)[source]

Build and write one stratum’s embedded project (SPEC §13).

Parameters:
Raises:

QgsProcessingException – If writing the project fails.

Return type:

None

stratified_packager.processing.project_builder.index_join_columns(source, plan, feedback)[source]

Index the columns this stratum’s re-pointed layers join on, in its gpkg (SPEC §13).

Both routes that keep a query live against the package — a live virtual layer and a §4 project_only layer whose |subset= is a SELECT — re-run that whole query for every feature count and every render (the canvas extent filter wraps the query rather than entering it), and QGIS pushes each equality in it down as one filtered request per outer row. Against the columns QgsVectorFileWriter leaves unindexed, every one of those requests is a full scan of the inner table, so the recipient pays a nested-loop join each time the layer is drawn. An index turns each request into a b-tree seek: measured 35x end to end on a 73k x 61k join (a feature count of 446s down to 13s), for indexes built in hundredths of a second and about one percent of the GeoPackage’s size.

One single-column index per column rather than one composite: QGIS stops at the first usable constraint, so only ever one column is pushed down and a composite index would serve only its leading column.

Best effort by design — an index that cannot be created costs the recipient speed, never correctness, so a failure is reported and the stratum ships regardless.

Parameters:
Return type:

None

stratified_packager.processing.project_builder.read_saved_view_extent(project_file)[source]

Read the last-saved map-canvas extent from a project file.

QGIS Desktop stores the canvas view under a top-level <mapcanvas> element; a headless write() never emits one, which is why generated projects open on the full layer extent rather than where the original was saved. This parses that element (preferring the primary theMapCanvas) straight from the .qgs XML — extracting it from the .qgz archive first — and returns its extent.

Parameters:

project_file (Path) – The source project file (.qgs or .qgz).

Return type:

tuple[float, float, float, float] | None

Returns:

(xmin, ymin, xmax, ymax) in the project CRS, or None when the file is not a readable project, carries no <mapcanvas> extent, or the extent is degenerate.

stratified_packager.processing.project_builder.resolve_initial_view(project)[source]

Resolve where the source project opens, for the embedded projects (§13).

Reproduces the original’s initial view: its saved map-canvas extent (read from the project file — the normal GUI-saved case), else its configured default view extent, else None (QGIS falls back to the full extent of the layers). The rectangle is returned in the project CRS, which the caller has already set on the embedded project, so applying it needs no reprojection.

Parameters:

project (QgsProject) – The open project being packaged.

Return type:

QgsReferencedRectangle | None

Returns:

The initial view rectangle, or None when none can be determined.

stratified_packager.processing.project_builder.snapshot_embedded_layers(layers)[source]

Serialize the embedded-only layers once for the whole run (SPEC §13).

Every stratum’s project carries the same embedded-only layers, and rebuilding one with clone() re-constructs its provider from the URI. For a remote provider that construction is a blocking network request — a WMS layer fetches GetCapabilities — so cloning charges the run one round-trip per layer per stratum, against a server whose answer never changes and which may not answer at all.

Serializing here instead makes _revive_embedded_layer() a pure XML operation. Live virtual layers are excluded: they are re-pointed at each stratum’s gpkg, not reused.

Parameters:

layers (Iterable[QgsMapLayer]) – The run’s embedded-only layers.

Return type:

dict[str, str]

Returns:

Layer id -> serialized <maplayer> document, for the layers that can be reproduced; the rest are absent and keep the clone path.

stratified_packager.processing.project_builder.validate_repointed_sources(layers, tables, /)[source]

Check the §4 project_only layers can be re-pointed at the stratum gpkg (§13).

Both guards are fatal at run start rather than at project-build time, where the only remaining move is to drop the layer — which ships a package quietly missing it.

The provider guard is the load-bearing one: re-pointing replaces everything before the first |, so a source that is not a file path (a database connection string) would be replaced whole, leaving a valid layer over the gpkg’s first table — wrong data, no error. The table guard catches the drift that makes such a layer stop resolving: it is authored against the names Phase A mints from the layer names, so renaming a packaged layer (or a §12 duplicate earning a _2 suffix) silently breaks it.

Parameters:
Raises:

qgis.core.QgsProcessingException – If a layer’s provider is not ogr, or its query reads a table this run does not create.

Return type:

None

stratified_packager.processing.project_builder._REVIVABLE_LAYER_TYPES: Final[dict[Qgis.LayerType, Callable[[], QgsMapLayer]]] = {qgis.core.Qgis.LayerType.Raster: qgis.core.QgsRasterLayer, qgis.core.Qgis.LayerType.Vector: qgis.core.QgsVectorLayer, qgis.core.Qgis.LayerType.VectorTile: qgis.core.QgsVectorTileLayer}

Layer types snapshot_embedded_layers() can reproduce from XML.

Each entry must construct an empty, source-less layer that readLayerXml() can then fill. Types absent here keep the clone() path — correct but, for a remote provider, a blocking network round-trip per stratum.

stratified_packager.processing.project_builder._TREE_NODE_SKIP_PROPERTIES: Final[frozenset[str]] = frozenset({'embedded', 'embedded_project'})

Layer-tree node custom properties never carried into the embedded project.

They mark a node whose children QGIS reloads from another project file on open (createEmbeddedGroup()), by an absolute path that does not ship beside the packaged data — the tree walk flattens such a node’s children into a plain group instead, and carrying the markers would send QGIS looking for the source machine’s project.

stratified_packager.processing.project_builder._WRITE_STEP: Final = '<project write>'

Name _report_slowest_steps() gives the project write, alongside the layer names.

Angle-bracketed like the SPEC’s other pseudo-names (<full>, <unmatched>), so it cannot collide with a layer.