Skip to content

Commit ffe0ff9

Browse files
committed
Add processing job runner
1 parent 0ef62cc commit ffe0ff9

8 files changed

Lines changed: 477 additions & 16 deletions

File tree

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
services:
22
qgis-server-light:
33
image: local/opengisch/qgis-server-light-dev:latest
4+
platform: linux/amd64
45
build:
56
context: .
67
dockerfile: ./Dockerfile

requirements.test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pillow
33
pixelmatch
44
mypy
55
pytest-cov
6+
pillow

src/qgis_server_light/exporter/cli.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
import os.path
33

44
import click
5+
from qgis.analysis import QgsNativeAlgorithms
56
from qgis.core import QgsApplication
67
from xsdata.formats.dataclass.serializers import JsonSerializer, XmlSerializer
78
from xsdata.formats.dataclass.serializers.config import SerializerConfig
89

910
from qgis_server_light.exporter.common import create_full_pg_service_conf
1011
from qgis_server_light.exporter.extract import Exporter
12+
from qgis_server_light.interface.exporter.extract import Process
13+
from qgis_server_light.worker.runner.process import algorithm_from_qgs_definition
1114

1215
os.environ["QT_QPA_PLATFORM"] = "offscreen"
1316
QgsApplication.setPrefixPath("/usr", True)
@@ -93,5 +96,28 @@ def export(
9396
raise AttributeError("Project file does not exist")
9497

9598

99+
@cli.command("export-processes")
100+
def export_processes():
101+
serializer_config = SerializerConfig(indent=" ")
102+
registry = qgs.processingRegistry()
103+
registry.addProvider(QgsNativeAlgorithms())
104+
process = Process(
105+
algorithms=[
106+
algorithm_from_qgs_definition(registry.algorithmById(alg))
107+
for alg in [
108+
"native:buffer",
109+
"native:centroids",
110+
"native:concavehull",
111+
"native:rasterlayerproperties",
112+
"native:rescaleraster",
113+
"native:collect",
114+
"native:rasterize",
115+
"native:affinetransform",
116+
]
117+
]
118+
)
119+
click.echo(JsonSerializer(config=serializer_config).render(process))
120+
121+
96122
if __name__ == "__main__":
97-
export()
123+
cli()

src/qgis_server_light/interface/exporter/extract.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from abc import ABC
88
from dataclasses import dataclass, field, fields
99
from datetime import UTC, datetime
10-
from typing import List, Optional
10+
from typing import Any, List, Optional
1111

1212
from qgis_server_light.interface.common import BaseInterface, BBox, Style
1313

@@ -638,3 +638,49 @@ class Config(BaseInterface):
638638
meta_data: MetaData = field(metadata={"type": "Element"})
639639
tree: Tree = field(metadata={"type": "Element"})
640640
datasets: Datasets = field(metadata={"type": "Element"})
641+
642+
643+
@dataclass
644+
class Parameter(BaseInterface):
645+
name: str = field(metadata={"type": "Element"})
646+
type: str = field(metadata={"type": "Element"})
647+
schema: dict = field(metadata={"type": "Attributes"})
648+
optional: bool = field(metadata={"type": "Element"})
649+
default: Any = field(metadata={"type": "Element"}, default=None)
650+
description: str | None = field(default=None, metadata={"type": "Element"})
651+
652+
@property
653+
def shortened_fields(self) -> set:
654+
return {"description"}
655+
656+
657+
@dataclass
658+
class Output(BaseInterface):
659+
name: str = field(metadata={"type": "Element"})
660+
type: str = field(metadata={"type": "Element"})
661+
schema: dict = field(metadata={"type": "Attributes"})
662+
description: str | None = field(default=None, metadata={"type": "Element"})
663+
664+
@property
665+
def shortened_fields(self) -> set:
666+
return {"description"}
667+
668+
669+
@dataclass
670+
class Algorithm:
671+
id: str = field(metadata={"type": "Element"})
672+
name: str = field(metadata={"type": "Element"})
673+
display_name: str = field(metadata={"type": "Element"})
674+
help_string: str | None = field(default=None, metadata={"type": "Element"})
675+
parameters: list[Parameter] = field(
676+
default_factory=list, metadata={"type": "Element"}
677+
)
678+
outputs: list[Output] = field(default_factory=list, metadata={"type": "Element"})
679+
680+
681+
@dataclass
682+
class Process:
683+
# uniqueness is not assured here!
684+
algorithms: list[Algorithm] = field(
685+
default_factory=list, metadata={"type": "Element"}
686+
)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from dataclasses import dataclass, field
2+
from typing import Any
3+
4+
from qgis_server_light.interface.job.common.input import (
5+
QslJobInfoParameter,
6+
QslJobParameter,
7+
)
8+
9+
10+
@dataclass(kw_only=True)
11+
class QslJobParameterExecuteProcess(QslJobParameter):
12+
"""A runner to execute a process"""
13+
14+
process_id: str = field(metadata={"type": "Element"})
15+
parameters: dict[str, Any] = field(metadata={"type": "Element"})
16+
17+
18+
@dataclass
19+
class QslJobInfoExecuteProcess(QslJobInfoParameter):
20+
job: QslJobParameterExecuteProcess = field(
21+
metadata={"type": "Element", "required": True}
22+
)
Lines changed: 183 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,199 @@
1-
import logging
1+
import sys
22
from typing import Optional
33

4-
from qgis.analysis import QgsNativeAlgorithms, QgsPdalAlgorithms
5-
from qgis.core import Qgis, QgsApplication, QgsProviderRegistry
6-
from qgis.processing import ProcessingAlgFactory
4+
from qgis.analysis import QgsNativeAlgorithms
5+
from qgis.core import (
6+
Qgis,
7+
QgsApplication,
8+
QgsProcessingAlgorithm,
9+
QgsProcessingContext,
10+
QgsProcessingFeedback,
11+
QgsProcessingOutputBoolean,
12+
QgsProcessingOutputDefinition,
13+
QgsProcessingOutputNumber,
14+
QgsProcessingOutputRasterLayer,
15+
QgsProcessingOutputString,
16+
QgsProcessingOutputVectorLayer,
17+
QgsProcessingParameterBand,
18+
QgsProcessingParameterBoolean,
19+
QgsProcessingParameterDefinition,
20+
QgsProcessingParameterEnum,
21+
QgsProcessingParameterExtent,
22+
QgsProcessingParameterFeatureSink,
23+
QgsProcessingParameterFeatureSource,
24+
QgsProcessingParameterField,
25+
QgsProcessingParameterMapTheme,
26+
QgsProcessingParameterMultipleLayers,
27+
QgsProcessingParameterNumber,
28+
QgsProcessingParameterRasterDestination,
29+
QgsProcessingParameterRasterLayer,
30+
QgsProcessingParameterString,
31+
)
732

8-
from qgis_server_light.interface.job.common.input import QslJobInfoParameter
33+
from qgis_server_light.interface.exporter.extract import (
34+
Algorithm,
35+
Output,
36+
Parameter,
37+
Process,
38+
)
39+
from qgis_server_light.interface.job.common.input import QslJobLayer
40+
from qgis_server_light.interface.job.common.output import JobResult
41+
from qgis_server_light.interface.job.process.input import (
42+
QslJobInfoExecuteProcess,
43+
QslJobParameterExecuteProcess,
44+
)
945
from qgis_server_light.worker.runner.common import JobContext, MapRunner
1046

1147

48+
def parameter_from_qgs_definition(param: QgsProcessingParameterDefinition) -> Parameter:
49+
if isinstance(param, QgsProcessingParameterFeatureSource):
50+
schema = {"type": "string"}
51+
elif isinstance(param, QgsProcessingParameterRasterLayer):
52+
schema = {"type": "string"}
53+
elif isinstance(param, QgsProcessingParameterFeatureSink):
54+
schema = {"type": "string"}
55+
elif isinstance(param, QgsProcessingParameterMultipleLayers):
56+
schema = {"type": "array", "items": {"type": "string"}}
57+
if (min_items := param.minimumNumberInputs()) >= 1:
58+
schema["minItems"] = min_items
59+
elif isinstance(param, QgsProcessingParameterRasterDestination):
60+
schema = {"type": "string"}
61+
elif isinstance(param, QgsProcessingParameterBand):
62+
schema = {"type": "integer", "minimum": 1}
63+
if param.allowMultiple():
64+
schema = {"type": "array", "minItems": 1, "items": schema}
65+
elif isinstance(param, QgsProcessingParameterNumber):
66+
match param.dataType():
67+
case Qgis.ProcessingNumberParameterType.Double:
68+
schema = {"type": "number"}
69+
case Qgis.ProcessingNumberParameterType.Integer:
70+
schema = {"type": "integer"}
71+
if (maximum := param.maximum()) < sys.float_info.max:
72+
schema["maximum"] = maximum
73+
if (minimum := param.minimum()) > sys.float_info.min:
74+
schema["minimum"] = minimum
75+
elif isinstance(param, QgsProcessingParameterString):
76+
schema = {"type": "string"}
77+
elif isinstance(param, QgsProcessingParameterField):
78+
schema = {"type": "string"}
79+
if param.allowMultiple():
80+
schema = {"type": "array", "minItems": 1, "items": schema}
81+
elif isinstance(param, QgsProcessingParameterEnum):
82+
schema = {"type": "string", "enum": param.options()}
83+
elif isinstance(param, QgsProcessingParameterBoolean):
84+
schema = {"type": "boolean"}
85+
elif isinstance(param, QgsProcessingParameterExtent):
86+
schema = {
87+
"oneOf": [
88+
{
89+
"type": "array",
90+
"items": {"type": "number"},
91+
"minItems": 4,
92+
"maxItems": 4,
93+
},
94+
{
95+
"type": "array",
96+
"items": {"type": "number"},
97+
"minItems": 6,
98+
"maxItems": 6,
99+
},
100+
]
101+
}
102+
elif isinstance(param, QgsProcessingParameterMapTheme):
103+
schema = {"type": "string"}
104+
else:
105+
print(f"parameter: {param}")
106+
raise NotImplementedError(f"parameter: {param}")
107+
108+
return Parameter(
109+
name=param.name(),
110+
type=param.type(),
111+
description=param.description(),
112+
schema=schema,
113+
optional=bool(param.flags() & Qgis.ProcessingParameterFlag.Optional),
114+
default=param.defaultValue(),
115+
)
116+
117+
118+
def output_from_qgs_definition(output: QgsProcessingOutputDefinition) -> Output:
119+
if isinstance(output, QgsProcessingOutputVectorLayer):
120+
schema = {"type": "string"}
121+
elif isinstance(output, QgsProcessingOutputRasterLayer):
122+
schema = {"type": "string"}
123+
elif isinstance(output, QgsProcessingOutputNumber):
124+
schema = {"type": "number"}
125+
elif isinstance(output, QgsProcessingOutputString):
126+
schema = {"type": "string"}
127+
elif isinstance(output, QgsProcessingOutputBoolean):
128+
schema = {"type": "boolean"}
129+
else:
130+
print(f"output: {output}")
131+
raise NotImplementedError(f"output: {output}")
132+
return Output(
133+
name=output.name(),
134+
type=output.type(),
135+
description=output.description(),
136+
schema=schema,
137+
)
138+
139+
140+
def algorithm_from_qgs_definition(alg: QgsProcessingAlgorithm) -> Algorithm:
141+
algorithm = Algorithm(
142+
id=alg.id(),
143+
name=alg.name(),
144+
display_name=alg.displayName(),
145+
help_string=alg.helpString(),
146+
)
147+
for param in alg.parameterDefinitions():
148+
algorithm.parameters.append(parameter_from_qgs_definition(param))
149+
for output in alg.outputDefinitions():
150+
algorithm.outputs.append(output_from_qgs_definition(output))
151+
return algorithm
152+
153+
12154
class ProcessRunner(MapRunner):
155+
job_info_class = QslJobInfoExecuteProcess
156+
13157
def __init__(
14158
self,
15159
qgis: QgsApplication,
16160
context: JobContext,
17-
job_info: QslJobInfoParameter,
161+
job_info: QslJobInfoExecuteProcess,
18162
layer_cache: Optional[dict],
19163
):
20164
super().__init__(qgis, context, job_info, layer_cache)
165+
self.registry = self.qgis.processingRegistry()
166+
self.registry.addProvider(QgsNativeAlgorithms())
167+
168+
@classmethod
169+
def info(cls, qgis: Qgis) -> Process:
170+
registry = qgis.processingRegistry()
171+
algorithms = registry.algorithms()
172+
process = Process()
173+
for alg in algorithms:
174+
algorithm = algorithm_from_qgs_definition(alg)
175+
process.algorithms.append(algorithm)
176+
return process
177+
178+
def run(self):
179+
job: QslJobParameterExecuteProcess = self.job_info.job
180+
algorithm = self.registry.algorithmById(job.process_id)
181+
if algorithm is None:
182+
return JobResult(
183+
id=self.job_info.id,
184+
data={"result": {}, "ok": False, "log": "Algorithm not found"},
185+
content_type="application/json",
186+
)
21187

22-
ProcessingAlgFactory()
23-
providers = QgsProviderRegistry.instance().pluginList().split("\n")
24-
logging.info("Found Providers:")
25-
for provider in providers:
26-
logging.info(f" - {provider}")
188+
for param, value in job.parameters.items():
189+
if isinstance(value, QslJobLayer):
190+
job.parameters[param] = self._handle_layer_cache(value)
27191

28-
def load_providers(self, qgis: Qgis):
29-
qgis.processingRegistry().addProvider(QgsNativeAlgorithms())
30-
qgis.processingRegistry().addProvider(QgsPdalAlgorithms())
192+
context = QgsProcessingContext()
193+
feedback = QgsProcessingFeedback()
194+
result, ok = algorithm.run(job.parameters, context, feedback)
195+
return JobResult(
196+
id=self.job_info.id,
197+
data={"result": result, "ok": ok, "log": feedback.textLog()},
198+
content_type="application/json",
199+
)

0 commit comments

Comments
 (0)