Skip to content

Commit 0de560e

Browse files
committed
merge main
2 parents f423838 + e916a6c commit 0de560e

File tree

10 files changed

+331
-11
lines changed

10 files changed

+331
-11
lines changed

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,23 @@ You can run the tests with following setup:
7474
# Then start containers for testing
7575
docker compose -f "docker/docker-compose.yml" up -d --build
7676

77+
# enter docker
78+
docker exec -it docker-actinia-ogc-api-processes-1 sh
7779

7880
# run all tests
79-
docker exec -t docker-actinia-ogc-api-processes-1 make test
81+
make test
8082

8183
# run only unittests
82-
docker exec -t docker-actinia-ogc-api-processes-1 make unittest
84+
make unittest
8385

8486
# run only integrationtests
85-
docker exec -t docker-actinia-ogc-api-processes-1 make integrationtest
87+
make integrationtest
8688

8789
# run only tests which are marked for development with the decorator '@pytest.mark.dev'
88-
docker exec -t docker-actinia-ogc-api-processes-1 make devtest
90+
make devtest
8991

92+
# or for debugging
93+
pytest -m devtest --pdb
9094

9195
# Stop containers after finishing testing
9296
docker compose -f "docker/docker-compose.yml" down

docker/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ services:
2727
actinia-core:
2828
image: mundialis/actinia:2.14.0
2929
volumes:
30-
- ./actinia-data/resources:/actinia_core/resources
30+
- ./actinia-data/resources/actinia-gdi/resource_id-8c88e3aa-3ba2-4050-9c2f-b3ad0a60f880:/actinia_core/resources/actinia-gdi/resource_id-8c88e3aa-3ba2-4050-9c2f-b3ad0a60f880:ro
3131
# ports:
3232
# - "8088:8088"
3333
depends_on:

src/actinia_ogc_api_processes_plugin/core/process_execution.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
__maintainer__ = "mundialis GmbH & Co. KG"
1313

1414

15+
from pathlib import Path
16+
1517
import requests
1618
from flask import has_request_context, jsonify, make_response, request
1719
from requests.auth import HTTPBasicAuth
@@ -33,6 +35,21 @@
3335
"v": "vector",
3436
}
3537

38+
RASTER_SUFFIXES = {
39+
".tif",
40+
".tiff",
41+
".jp2",
42+
".vrt",
43+
".zip",
44+
}
45+
VECTOR_SUFFIXES = {
46+
".geojson",
47+
".gpkg",
48+
".json",
49+
".gml",
50+
".zip",
51+
}
52+
3653

3754
def generate_new_joblinks(job_id: str) -> list[dict]:
3855
"""Make sure job_id is in the link."""
@@ -221,13 +238,22 @@ def _invalid_inputs(module_info: dict, inputs: dict):
221238
else {}
222239
)
223240
expected = schema.get("type")
241+
subtype = schema.get("subtype", None)
224242

225243
# if no explicit type in schema, accept the input
226244
if not expected:
227245
continue
228246

229247
if expected == "string":
230-
if not isinstance(val, str):
248+
# for raster (cell) / vector type, accept links
249+
if subtype in {"cell", "vector"}:
250+
msg_append += _validate_string_input(
251+
invalid,
252+
key,
253+
val,
254+
subtype,
255+
)
256+
elif not isinstance(val, str):
231257
invalid.append(key)
232258
elif expected == "boolean":
233259
if not isinstance(val, bool):
@@ -285,6 +311,67 @@ def is_valid_postbody(postbody: dict) -> bool:
285311
return not wrong_key
286312

287313

314+
def _validate_string_input(
315+
invalid: list,
316+
key: str,
317+
val: str | dict,
318+
subtype: str,
319+
) -> str:
320+
"""Validate string input, which can be a link for raster/ vector type."""
321+
msg_append = ""
322+
if isinstance(val, dict) and "href" in val:
323+
val["subtype"] = "raster" if subtype == "cell" else "vector"
324+
path = Path(val["href"])
325+
suffix = path.suffix.lower()
326+
if subtype == "cell" and suffix not in RASTER_SUFFIXES:
327+
msg_append += (
328+
f"Raster input parameter '{key}' has suffix "
329+
f"'{suffix}' which is not supported."
330+
)
331+
invalid.append(key)
332+
elif subtype == "vector" and suffix not in VECTOR_SUFFIXES:
333+
msg_append += (
334+
f"Vector input parameter '{key}' has suffix "
335+
f"'{suffix}' which is not supported."
336+
)
337+
invalid.append(key)
338+
elif not isinstance(val, str):
339+
invalid.append(key)
340+
return msg_append
341+
342+
343+
def _check_input_by_reference(postbody: dict) -> dict:
344+
"""Check for input by reference.
345+
346+
If input by reference is used the postbody is adjusted and the generated
347+
importer is returned. If no input by reference used, None is returned.
348+
"""
349+
inputs = postbody.get("inputs", {})
350+
importer = {
351+
"id": "importer_1",
352+
"module": "importer",
353+
"inputs": [],
354+
}
355+
for key, value in inputs.items():
356+
if isinstance(value, dict) and "href" in value:
357+
# input by reference
358+
importer["inputs"].append(
359+
{
360+
"import_descr": {
361+
"source": value["href"],
362+
"type": value.get("subtype"),
363+
},
364+
"param": "map",
365+
"value": f"{key}_map",
366+
},
367+
)
368+
inputs[key] = f"{key}_map"
369+
if importer["inputs"]:
370+
return importer
371+
else:
372+
return None
373+
374+
288375
def post_process_execution(
289376
process_id: str | None = None,
290377
postbody: dict | None = None,
@@ -355,6 +442,10 @@ def post_process_execution(
355442
if postbody.get("inputs") and postbody.get("inputs").get("project"):
356443
project_name = postbody.get("inputs").get("project")
357444

445+
# check for input by reference and create importer
446+
importer = _check_input_by_reference(postbody)
447+
448+
# transform postbody to actinia process chain format
358449
pc = _transform_to_actinia_process_chain(process_id, postbody)
359450

360451
# adjust pc if process is grass module
@@ -413,6 +504,11 @@ def post_process_execution(
413504
bounding_box,
414505
)
415506

507+
# add importer at the beginning of the process chain list if input by
508+
# reference
509+
if importer:
510+
pc["list"].insert(0, importer)
511+
416512
# Start process via actinia-module-plugin
417513
kwargs["json"] = pc
418514
url_process_execution = (

tests/integrationtests/test_processes_execution.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ def test_post_process_execution_with_bbox(self) -> None:
131131
assert "message" in resp.json
132132

133133
@pytest.mark.integrationtest
134-
@pytest.mark.dev
135134
def test_post_process_execution_with_bbox_error(self) -> None:
136135
"""Test post method of the /processes/<process_id>/execution endpoint.
137136
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env python
2+
"""SPDX-FileCopyrightText: (c) 2026 by mundialis GmbH & Co. KG.
3+
4+
SPDX-License-Identifier: GPL-3.0-or-later
5+
"""
6+
7+
__license__ = "GPL-3.0-or-later"
8+
__author__ = "Anika Weinmann"
9+
__copyright__ = "Copyright 2026 mundialis GmbH & Co. KG"
10+
__maintainer__ = "mundialis GmbH & Co. KG"
11+
12+
13+
import json
14+
import pathlib
15+
16+
import pytest
17+
from flask import Response
18+
19+
from tests.testsuite import TestCase
20+
21+
test_process_input_error = {
22+
"inputs": {
23+
"input": {
24+
"href": "https://raw.githubusercontent.com/mmacata/pagestest/"
25+
"refs/heads/gh-pages/kalimantan_osm_epsg4326.geojson",
26+
},
27+
"size": 3,
28+
"method": ["maximum"],
29+
"output": ["slope_n3_max"],
30+
},
31+
"outputs": {"result": {"transmissionMode": "reference"}},
32+
"response": "document",
33+
}
34+
35+
36+
class ProcessExecutionInputByReference(TestCase):
37+
"""Test class for executing Processes with input by reference.
38+
39+
For /processes/<process_id>/execute endpoint.
40+
"""
41+
42+
@pytest.mark.integrationtest
43+
def test_post_process_execution_vbuffer(self) -> None:
44+
"""Test post method of the /processes/<process_id>/execution endpoint.
45+
46+
Succesfull query v.buffer process
47+
"""
48+
json_path = (
49+
"tests/resources/request_bodies/test_v_buffer_by_reference.json"
50+
)
51+
with pathlib.Path(json_path).open(encoding="utf-8") as file:
52+
request_body = json.load(file)
53+
54+
resp = self.app.post(
55+
"/processes/v.buffer/execution",
56+
headers=self.HEADER_AUTH,
57+
json=request_body,
58+
)
59+
assert isinstance(resp, Response)
60+
assert resp.status_code == 201
61+
assert hasattr(resp, "json")
62+
assert "message" in resp.json
63+
assert resp.json["message"] == "Resource accepted"
64+
# response should follow StatusInfoResponseModel:
65+
# contain jobID, status and links
66+
assert isinstance(resp.json, dict)
67+
assert "jobID" in resp.json
68+
assert "status" in resp.json
69+
assert resp.json["status"] in {
70+
"accepted",
71+
"running",
72+
"successful",
73+
"failed",
74+
"dismissed",
75+
}
76+
assert "links" in resp.json
77+
assert isinstance(resp.json["links"], list)
78+
# link should point to job status resource
79+
if resp.json["links"]:
80+
assert any(
81+
"/jobs/" in link.get("href", "") for link in resp.json["links"]
82+
)
83+
84+
@pytest.mark.integrationtest
85+
def test_post_process_execution_rneighbors(self) -> None:
86+
"""Test post method of the /processes/<process_id>/execution endpoint.
87+
88+
Succesfull query r.neighbors process with input by reference
89+
"""
90+
json_path = (
91+
"tests/resources/request_bodies/test_r_neighbors_by_reference.json"
92+
)
93+
with pathlib.Path(json_path).open(encoding="utf-8") as file:
94+
request_body = json.load(file)
95+
96+
resp = self.app.post(
97+
"/processes/r.neighbors/execution",
98+
headers=self.HEADER_AUTH,
99+
json=request_body,
100+
)
101+
assert isinstance(resp, Response)
102+
assert resp.status_code == 201
103+
assert hasattr(resp, "json")
104+
assert "message" in resp.json
105+
assert resp.json["message"] == "Resource accepted"
106+
107+
@pytest.mark.integrationtest
108+
def test_post_process_execution_vclip(self) -> None:
109+
"""Test post method of the /processes/<process_id>/execution endpoint.
110+
111+
Succesfull query v.clip process with input by reference
112+
"""
113+
json_path = (
114+
"tests/resources/request_bodies/test_v_clip_by_reference.json"
115+
)
116+
with pathlib.Path(json_path).open(encoding="utf-8") as file:
117+
request_body = json.load(file)
118+
119+
resp = self.app.post(
120+
"/processes/v.clip/execution",
121+
headers=self.HEADER_AUTH,
122+
json=request_body,
123+
)
124+
assert isinstance(resp, Response)
125+
assert resp.status_code == 201
126+
assert hasattr(resp, "json")
127+
assert "message" in resp.json
128+
assert resp.json["message"] == "Resource accepted"
129+
130+
@pytest.mark.integrationtest
131+
def test_post_process_execution_input_by_ref_error(self) -> None:
132+
"""Test post method of the /processes/<process_id>/execution endpoint.
133+
134+
Failing query with input by reference
135+
"""
136+
resp = self.app.post(
137+
"/processes/r.neighbors/execution",
138+
headers=self.HEADER_AUTH,
139+
json=test_process_input_error,
140+
)
141+
assert isinstance(resp, Response)
142+
assert resp.status_code == 400
143+
assert hasattr(resp, "json")
144+
assert "message" in resp.json
145+
assert "'.geojson' which is not supported." in resp.json["message"]

tests/resources/request_bodies/README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ curl -X POST \
6161
-d @test_get_county_hospitals.json ${BASEURL}/processes/get_county_hospitals/execution
6262
```
6363

64-
Simulation of a classification error matrix as example for text file export with ``:
64+
Simulation of a classification error matrix as example for text file export with `classification_error_matrix`:
6565

6666
```bash
6767
curl -X POST \
@@ -70,11 +70,38 @@ curl -X POST \
7070
-d @test_classification_error_matrix.json ${BASEURL}/processes/classification_error_matrix/execution
7171
```
7272

73-
Example actinia-module for stdout with ``:
73+
Example actinia-module for stdout with `elevation_stdout`:
7474

7575
```bash
7676
curl -X POST \
7777
-H 'Content-Type: application/json' -H 'accept: application/json' \
7878
-u ${AUTH} \
7979
-d @test_elevation_stdout.json ${BASEURL}/processes/elevation_stdout/execution
8080
```
81+
82+
Example point buffering with **input by reference**:
83+
84+
```bash
85+
curl -X POST \
86+
-H 'Content-Type: application/json' -H 'accept: application/json' \
87+
-u ${AUTH} \
88+
-d @test_v_buffer_by_reference.json ${BASEURL}/processes/v.buffer/execution
89+
```
90+
91+
Example vector clipping with **input by reference**:
92+
93+
```bash
94+
curl -X POST \
95+
-H 'Content-Type: application/json' -H 'accept: application/json' \
96+
-u ${AUTH} \
97+
-d @test_v_clip_by_reference.json ${BASEURL}/processes/v.clip/execution
98+
```
99+
100+
Raster neighbor hood example with **input by reference**:
101+
102+
```bash
103+
curl -X POST \
104+
-H 'Content-Type: application/json' -H 'accept: application/json' \
105+
-u ${AUTH} \
106+
-d @test_r_neighbors_by_reference.json ${BASEURL}/processes/r.neighbors/execution
107+
```

tests/resources/request_bodies/test_r_neighbors.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"inputs": {
33
"input": "elevation",
4-
"size": "3",
4+
"size": 3,
55
"method": ["maximum"],
6-
"output": "elevation_n3_max"
6+
"output": ["elevation_n3_max"]
77
},
88
"outputs": {
99
"result": {

0 commit comments

Comments
 (0)