WMS-Layer bauen, frische Liste holen, Vererbung im Headless-Test belegen

Drei Fehler, die erst der erweiterte Headless-Test gegen die echte Schnittstelle sichtbar
gemacht hat (11.09.2026, alle drei Anbieter plus Vererbung und Stil):

1. WMS-Layer kamen als "Provider is not valid" zurück. Der Rasterprovider erwartet seine
   Parameter mit "&" getrennt statt mit Leerzeichen wie die Vektorprovider — und "styles"
   muss dabei sein, auch leer. Beides durchgemessen am wms_geosn_dop-rgb: ohne "styles"
   ungültig, mit "&"+"styles=" gültig in 0,2 s. Ein fertiger URI wird durchgereicht.

2. "Dienste holen" lieferte bis zu eine Minute lang die alte Liste. Die Schnittstelle
   antwortet mit "Cache-Control: private, max-age=60", damit ein Ladelauf nicht alles neu
   holt — bei einem Klick des Nutzers ist das aber falsch: wer gerade im Manager einen
   Dienst eingetragen hat, will ihn sehen. dienste_laden und stil_anwenden fragen jetzt
   frisch.

3. Gesteuert wird das über den forceRefresh-Parameter von QgsBlockingNetworkRequest.get/post,
   NICHT über CacheLoadControlAttribute am Request: das Attribut wird intern überschrieben
   und blieb wirkungslos.

Der Test in tools/headless_dienste_test.py deckt jetzt auch ab, was das Plugin bisher nicht
sah, weil alle 44 Altdaten ArcGIS-REST sind: WMS und WFS bauen, Ausblenden wirkt und lässt
sich zurücknehmen, ein Stil bleibt beim Ändern ohne QML stehen und lässt sich entfernen.
Angelegte Proben räumt der Lauf selbst wieder ab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
erik
2026-09-11 18:29:25 +02:00
co-authored by Claude Opus 5
parent c8b723b653
commit 3486d3a74e
3 changed files with 133 additions and 8 deletions
+100
View File
@@ -82,6 +82,106 @@ if ohne:
ok, meldung = LL.stil_anwenden(lyr, BASE, ohne[0]["id"], KEY)
pruefe(not ok and "kein Stil" in meldung, "Meldung statt Fehler: %s" % meldung)
# ---------------------------------------------------------------------------------------
# Ab hier wird geschrieben: eigene Eintraege anlegen, die Vererbung pruefen, wieder aufraeumen.
# Das Plugin selbst schreibt nichts in die Linkliste — deshalb laufen die Pflegeaufrufe hier
# ueber urllib und nicht ueber den KartenApiClient (der kennt nur GET/POST/PUT).
# ---------------------------------------------------------------------------------------
import json as _json
import urllib.error
import urllib.request
def api(pfad, art="GET", rumpf=None):
"""Ein Pflegeaufruf gegen /v2/linkliste. Gibt (status, daten) zurueck."""
req = urllib.request.Request(BASE + "/linkliste" + pfad, method=art,
data=_json.dumps(rumpf).encode("utf-8") if rumpf is not None else None,
headers={"X-API-Key": KEY, "Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=20) as a:
roh = a.read()
return a.status, (_json.loads(roh) if roh else {})
except urllib.error.HTTPError as e:
return e.code, {}
def aufraeumen(ids):
for i in ids:
api("/%s" % i, "DELETE")
print("== 5) WMS und WFS bauen (die Altdaten sind alle ArcGIS) ==")
PROBEN = [
{"gruppe": "hintergrund", "name": "ZZ Probe WMS", "provider": "wms",
"url": "https://geodienste.sachsen.de/wms_geosn_dop-rgb/guest?", "layer": "sn_dop_020"},
{"gruppe": "sonstiges", "name": "ZZ Probe WFS", "provider": "wfs",
"url": "https://sgx.geodatenzentrum.de/wfs_vg2500", "layer": "vg2500:vg2500_krs"},
]
angelegt = []
try:
for probe in PROBEN:
st, d = api("", "POST", dict(probe, stufe="land"))
if st != 201:
pruefe(False, "%s anlegen: HTTP %s" % (probe["name"], st))
continue
angelegt.append(d["data"]["id"])
liste = LL.dienste_laden(client, VKZ)
for probe in PROBEN:
eintrag = next((x for x in liste if x["name"] == probe["name"]), None)
if eintrag is None:
pruefe(False, "%s steht nicht in der Liste" % probe["name"])
continue
t = time.time()
lyr = LL.layer_bauen(eintrag)
ok = lyr is not None and lyr.isValid()
pruefe(ok, "%-4s %-16s %s (%.1f s)"
% (probe["provider"], probe["name"][-12:],
"gueltig" if ok else (lyr.error().message()[:70] if lyr else "kein Layer"),
time.time() - t))
if ok and probe["provider"] == "wfs" and hasattr(lyr, "featureCount"):
print(" Features:", lyr.featureCount())
print("== 6) Vererbung: Ausblenden wirkt, Zuruecknehmen holt zurueck ==")
ziel = angelegt[0] if angelegt else None
bereich = {"stufe": "tg", "vkz": int(VKZ), "team_id": None}
if ziel:
st, _ = api("/%s/abweichung" % ziel, "POST",
{"bereich": bereich, "art": "aus", "felder": {}})
pruefe(st in (200, 201), "ausblenden fuer das Verfahren: HTTP %s" % st)
namen = [x["name"] for x in LL.dienste_laden(client, VKZ)]
pruefe("ZZ Probe WMS" not in namen,
"ausgeblendeter Dienst kommt nicht mehr im Plugin an (%d Dienste)" % len(namen))
st, _ = api("/%s/abweichung?stufe=tg&vkz=%s&team_id=" % (ziel, VKZ), "DELETE")
pruefe(st == 204, "zuruecknehmen: HTTP %s" % st)
namen = [x["name"] for x in LL.dienste_laden(client, VKZ)]
pruefe("ZZ Probe WMS" in namen, "nach dem Zuruecknehmen wieder da (%d Dienste)" % len(namen))
print("== 7) Stil setzen und wieder entfernen ==")
if ziel:
st, _ = api("/%s" % ziel, "PATCH",
dict(PROBEN[0], stufe="land",
qml="<qgis version=\"3.34\"><renderer-v2 type=\"singleSymbol\"/></qgis>"))
pruefe(st == 200, "Stil ueber PATCH setzen: HTTP %s" % st)
eintrag = next((x for x in LL.dienste_laden(client, VKZ) if x["name"] == "ZZ Probe WMS"), None)
pruefe(bool(eintrag and eintrag.get("stil")), "die Liste meldet den Stil")
# Aendern ohne QML darf den Stil NICHT loeschen — sonst verliert jede Korrektur den Stil.
st, _ = api("/%s" % ziel, "PATCH", dict(PROBEN[0], stufe="land", beschreibung="ohne QML"))
eintrag = next((x for x in LL.dienste_laden(client, VKZ) if x["name"] == "ZZ Probe WMS"), None)
pruefe(bool(eintrag and eintrag.get("stil")), "Aendern ohne QML laesst den Stil stehen")
st, _ = api("/%s/style.qml" % ziel, "DELETE")
pruefe(st == 204, "Stil entfernen: HTTP %s" % st)
lyr = LL.layer_bauen(eintrag) if eintrag else None
if lyr is not None and lyr.isValid():
ok, meldung = LL.stil_anwenden(lyr, BASE, ziel, KEY)
pruefe(not ok and "kein Stil" in meldung,
"nach dem Entfernen meldet das Plugin sauber: %s" % meldung)
finally:
aufraeumen(angelegt)
rest = [x["name"] for x in LL.dienste_laden(client, VKZ) if x["name"].startswith("ZZ ")]
pruefe(not rest, "aufgeraeumt, keine Testreste" + (" — offen: %s" % rest if rest else ""))
app.exitQgis()
print("== %d Fehler ==" % len(FEHLER))
for f in FEHLER:
+15 -3
View File
@@ -142,7 +142,15 @@ class KartenApiClient:
"Antwort von %s ist keine GeoJSON FeatureCollection." % path
)
def _request(self, method, path, payload=None, with_auth=True):
def _request(self, method, path, payload=None, with_auth=True, frisch=False):
"""Ein Aufruf gegen die v2-API.
:param frisch: den Zwischenspeicher übergehen. Die Linkliste antwortet mit
``Cache-Control: private, max-age=60``, damit ein Ladelauf nicht alles neu holt.
Wer aber gerade im Manager einen Dienst eingetragen hat und dann in QGIS auf
„Dienste holen“ klickt, bekäme sonst bis zu eine Minute lang die alte Liste
(gemessen 11.09.2026). Bei einer ausdrücklichen Nutzeraktion also immer frisch.
"""
if with_auth and not self.is_authenticated:
raise AuthError("Nicht angemeldet — bitte zuerst einloggen.")
@@ -158,11 +166,15 @@ class KartenApiClient:
if payload is not None:
body = json.dumps(payload).encode("utf-8")
# Der Zwischenspeicher wird über forceRefresh gesteuert, NICHT über
# CacheLoadControlAttribute am Request: QgsBlockingNetworkRequest setzt das Attribut
# selbst und überschreibt dabei, was man vorher gesetzt hat (gemessen 11.09.2026 —
# ein AlwaysNetwork am Request blieb wirkungslos).
blocking = QgsBlockingNetworkRequest()
if method == "GET":
error = blocking.get(request)
error = blocking.get(request, frisch)
elif method == "POST":
error = blocking.post(request, body)
error = blocking.post(request, body, frisch)
elif method == "PUT":
error = blocking.put(request, body)
else:
+18 -5
View File
@@ -49,9 +49,13 @@ WMS_CRS = "EPSG:25833"
def dienste_laden(client, vkz):
"""``GET /linkliste?vkz=…`` -> Liste der aufgelösten Einträge.
Immer frisch: die Antwort trägt ``Cache-Control: max-age=60``, damit ein Ladelauf nicht
alles neu holt. „Dienste holen“ ist aber ein Klick des Nutzers — wer gerade im Manager
etwas eingetragen hat, will es hier sehen und nicht die Liste von vorhin.
:raises ApiError: wenn die Antwort nicht das erwartete Format hat.
"""
daten = client._request("GET", "/linkliste?vkz=%s" % vkz)
daten = client._request("GET", "/linkliste?vkz=%s" % vkz, frisch=True)
zeilen = daten.get("dienste") if isinstance(daten, dict) else None
if not isinstance(zeilen, list):
raise ApiError("Antwort von /linkliste hat ein unerwartetes Format.")
@@ -70,8 +74,11 @@ def stil_anwenden(layer, base_url, eintrag_id, api_key=None):
req = QNetworkRequest(QUrl(stil_url(base_url, eintrag_id)))
if api_key:
req.setRawHeader(b"X-API-Key", api_key.encode("utf-8"))
# Auch der Stil kommt mit max-age=60. Wer ihn im Manager gerade getauscht oder entfernt
# hat, würde sonst beim nächsten Laden noch den alten sehen — beim Klick also frisch.
# Gesteuert wird das über forceRefresh, ein Attribut am Request wird überschrieben.
blk = QgsBlockingNetworkRequest()
fehler = blk.get(req)
fehler = blk.get(req, True)
antwort = blk.reply()
code = antwort.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
# Ein 404 kommt beim blockierenden Aufruf als Transportfehler an — erst den Statuscode
@@ -116,9 +123,15 @@ def layer_bauen(eintrag, zeit_ms=20000):
pass
if provider == "wms":
uri = url if url.lower().startswith("url=") else "url=" + url
if ebene and "layers=" not in uri:
uri = "layers=%s format=%s crs=%s %s" % (ebene, WMS_FORMAT, WMS_CRS, uri)
# WMS ist der Sonderfall: der Rasterprovider erwartet die Parameter mit **&** getrennt,
# nicht mit Leerzeichen wie die Vektorprovider — und **styles muss dabei sein**, auch
# leer. Ohne beides kommt der Layer als „Provider is not valid“ zurück, ohne dass
# irgendwo eine Adresse falsch wäre (durchgemessen 11.09.2026 am wms_geosn_dop-rgb).
basis = url[4:] if url.lower().startswith("url=") else url
if "&" in basis and "url=" in basis: # schon ein fertiger URI
return QgsRasterLayer(basis, name, "wms")
uri = "crs=%s&dpiMode=7&format=%s&layers=%s&styles=&url=%s" % (
WMS_CRS, WMS_FORMAT, ebene, basis)
return QgsRasterLayer(uri, name, "wms")
if provider == "wfs":