fixed sonarqube scan issues
Sonarqube Scanner / Build and analyze (push) Failing after 22s

This commit is contained in:
Florian Zumpe
2026-06-17 15:12:46 +02:00
parent 2442f8d458
commit 95026aac6e
28 changed files with 973 additions and 184 deletions
+15 -1
View File
@@ -1,6 +1,6 @@
# Python-Werkzeuge für die Wegwichtel-REST-API
Die Skripte benötigen nur Python 3 und die Standardbibliothek. Node.js, `curl` und zusätzliche Python-Pakete sind auf dem aufrufenden Rechner nicht erforderlich.
Die Skripte benötigen für den regulären Betrieb nur Python 3 und die Standardbibliothek. Node.js, `curl` und zusätzliche Python-Pakete sind auf dem aufrufenden Rechner nicht erforderlich. Nur die automatisierte Coverage-Erzeugung verwendet das in `requirements-dev.txt` festgelegte Paket `coverage`.
## Authentifizierung
@@ -64,3 +64,17 @@ python3 tools/python/create_picture.py \
```
Die Multipart-Hilfsfunktion sendet für Dateien absichtlich keinen behaupteten MIME-Typ. Der Server bestimmt das Format aus dem Dateiinhalt.
## Fehlerausgabe und Tests
Alle Werkzeuge kapseln ihre Aktion in der gemeinsamen `run()`-Funktion. Bei API-, Datei-, Eingabe- oder unerwarteten Laufzeitfehlern werden der Fehlertyp, die Fehlermeldung und der vollständige Python-Stacktrace auf `stderr` ausgegeben. Der Prozess endet anschließend mit Exitcode `1`; ein manueller Abbruch per Tastatur verwendet Exitcode `130`.
Die Python-Tests und der Cobertura-kompatible Bericht werden über die NPM-Skripte ausgeführt:
```bash
python3 -m pip install -r requirements-dev.txt
npm run test:python
npm run test:coverage:python
```
Der Bericht liegt anschließend unter `coverage/python/coverage.xml` und wird über `sonar.python.coverage.reportPaths` von SonarQube eingelesen.
+20 -7
View File
@@ -1,14 +1,27 @@
#!/usr/bin/env python3
"""Hängt GPX-Punkte an eine vorhandene Route an."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, ask_path, client_from_args, run
parser = argparse.ArgumentParser(description="GPX-Punkte an eine Route anhängen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--gpx")
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="GPX-Punkte an eine Route anhängen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--gpx")
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
route_id = ask("Route-ID", args.route_id, required=True, cast=int)
gpx = ask_path("GPX-Datei", args.gpx)
return client.request("POST", f"/api/routes/{route_id}/append", files={"gpx": gpx})
run(action)
if __name__ == "__main__":
run(main)
+98 -43
View File
@@ -9,6 +9,7 @@ import getpass
import json
import os
import sys
import traceback
import uuid
from dataclasses import dataclass
from pathlib import Path
@@ -20,23 +21,30 @@ from urllib.request import Request, urlopen
DEFAULT_BASE_URL = "https://wegwichtel.de"
API_USER_ENV = "API_USER"
API_PASSWORD_ENV = "API_PASSWORD"
APPLICATION_JSON = "application/json"
MAX_AUTH_ATTEMPTS = 3
REQUEST_TIMEOUT_SECONDS = 120
class ApiError(RuntimeError):
"""Fehlerantwort der REST-API einschließlich HTTP-Status und Nutzdaten."""
def __init__(self, status: int, message: str, payload: Any = None):
super().__init__(f"HTTP {status}: {message}")
self.status = status
self.payload = payload
@dataclass
@dataclass(frozen=True)
class Credentials:
"""Zugangsdaten für eine nach HTTP 401 angeforderte Basic-Authentifizierung."""
username: str
password: str
def credentials_from_environment() -> Credentials | None:
"""Liest CI-Credentials, ohne einen Request vor dem ersten HTTP 401 zu authentifizieren."""
"""Liest CI-Credentials, ohne den ersten Request vorab zu authentifizieren."""
username = os.getenv(API_USER_ENV, "").strip()
password = os.getenv(API_PASSWORD_ENV, "").strip()
if username and password:
@@ -44,12 +52,35 @@ def credentials_from_environment() -> Credentials | None:
return None
def _json_bytes(value: dict[str, Any]) -> bytes:
return json.dumps(value, ensure_ascii=False).encode("utf-8")
def _request_payload(
json_body: dict[str, Any] | None,
fields: dict[str, Any] | None,
files: dict[str, Path] | None,
) -> tuple[bytes | None, dict[str, str]]:
headers = {"Accept": APPLICATION_JSON}
if files:
data, content_type = encode_multipart(fields or {}, files)
headers["Content-Type"] = content_type
return data, headers
if json_body is not None:
headers["Content-Type"] = APPLICATION_JSON
return _json_bytes(json_body), headers
if fields:
headers["Content-Type"] = APPLICATION_JSON
return _json_bytes(fields), headers
return None, headers
class ApiClient:
"""Kleine HTTP-Schicht mit dynamischer Basic-Auth-Nachfrage bei 401."""
"""Kleine HTTP-Schicht mit dynamischer Basic-Auth-Nachfrage bei HTTP 401."""
def __init__(self, base_url: str, username: str | None = None, password: str | None = None):
self.base_url = base_url.rstrip("/") + "/"
self.credentials = Credentials(username, password) if username and password else None
self.credentials = Credentials(username.strip(), password.strip()) if username and password else None
def prompt_credentials(self) -> None:
environment_credentials = credentials_from_environment()
@@ -57,21 +88,60 @@ class ApiClient:
self.credentials = environment_credentials
return
default_user = self.credentials.username if self.credentials else os.getenv(API_USER_ENV, "")
username = input(f"Benutzername{f' [{default_user}]' if default_user else ''}: ").strip() or default_user
default_user = self.credentials.username if self.credentials else os.getenv(API_USER_ENV, "").strip()
suffix = f" [{default_user}]" if default_user else ""
username = input(f"Benutzername{suffix}: ").strip() or default_user
if not username:
raise ApiError(401, "Für den geschützten API-Zugriff ist ein Benutzername erforderlich.")
password = getpass.getpass("Passwort: ")
password = getpass.getpass("Passwort: ").strip()
if not password:
raise ApiError(401, "Für den geschützten API-Zugriff ist ein Passwort erforderlich.")
self.credentials = Credentials(username, password)
def _authorization_header(self) -> str | None:
if not self.credentials:
return None
token = base64.b64encode(
f"{self.credentials.username}:{self.credentials.password}".encode("utf-8")
).decode("ascii")
value = f"{self.credentials.username}:{self.credentials.password}".encode("utf-8")
token = base64.b64encode(value).decode("ascii")
return f"Basic {token}"
def _request_headers(self, headers: dict[str, str]) -> dict[str, str]:
request_headers = dict(headers)
authorization = self._authorization_header()
if authorization:
request_headers["Authorization"] = authorization
return request_headers
def _perform_request(
self,
method: str,
url: str,
data: bytes | None,
headers: dict[str, str],
expected: tuple[int, ...],
) -> Any:
request = Request(
url,
data=data,
headers=self._request_headers(headers),
method=method.upper(),
)
with urlopen(request, timeout=REQUEST_TIMEOUT_SECONDS) as response:
raw = response.read()
if response.status not in expected:
raise ApiError(response.status, decode_error(raw), parse_json(raw))
return parse_response(raw, response.headers.get("Content-Type", ""))
def _handle_http_error(self, error: HTTPError, attempt: int) -> bool:
if error.code != 401 or attempt >= MAX_AUTH_ATTEMPTS:
return False
error.read()
error.close()
print("Die API verlangt Zugangsdaten (HTTP 401).", file=sys.stderr)
self.prompt_credentials()
return True
def request(
self,
method: str,
@@ -82,42 +152,18 @@ class ApiClient:
files: dict[str, Path] | None = None,
expected: tuple[int, ...] = (200, 201),
) -> Any:
data: bytes | None = None
headers = {"Accept": "application/json"}
if files:
data, content_type = encode_multipart(fields or {}, files)
headers["Content-Type"] = content_type
elif json_body is not None:
data = json.dumps(json_body, ensure_ascii=False).encode("utf-8")
headers["Content-Type"] = "application/json"
elif fields:
data = json.dumps(fields, ensure_ascii=False).encode("utf-8")
headers["Content-Type"] = "application/json"
data, headers = _request_payload(json_body, fields, files)
url = urljoin(self.base_url, path.lstrip("/"))
attempts = 0
while True:
request_headers = dict(headers)
authorization = self._authorization_header()
if authorization:
request_headers["Authorization"] = authorization
request = Request(url, data=data, headers=request_headers, method=method.upper())
attempt = 0
while True:
try:
with urlopen(request, timeout=120) as response:
raw = response.read()
status = response.status
if status not in expected:
raise ApiError(status, decode_error(raw), parse_json(raw))
return parse_response(raw, response.headers.get("Content-Type", ""))
return self._perform_request(method, url, data, headers, expected)
except HTTPError as error:
raw = error.read()
if error.code == 401 and attempts < 3:
print("Die API verlangt Zugangsdaten (HTTP 401).", file=sys.stderr)
self.prompt_credentials()
attempts += 1
if self._handle_http_error(error, attempt):
attempt += 1
continue
raw = error.read()
raise ApiError(error.code, decode_error(raw), parse_json(raw)) from error
except URLError as error:
raise RuntimeError(f"API nicht erreichbar: {error.reason}") from error
@@ -247,9 +293,18 @@ def print_result(result: Any) -> None:
print(json.dumps(result, ensure_ascii=False, indent=2) if not isinstance(result, str) else result)
def _print_failure(error: BaseException) -> None:
print(f"Fehler: {type(error).__name__}: {error}", file=sys.stderr)
traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)
def run(action: Callable[[], Any]) -> None:
"""Führt eine Werkzeugaktion aus und gibt Fehler samt vollständigem Stacktrace aus."""
try:
print_result(action())
except (ApiError, RuntimeError, FileNotFoundError, ValueError) as error:
print(f"Fehler: {error}", file=sys.stderr)
except KeyboardInterrupt as error:
_print_failure(error)
raise SystemExit(130) from error
except Exception as error:
_print_failure(error)
raise SystemExit(1) from error
+21 -8
View File
@@ -1,16 +1,29 @@
#!/usr/bin/env python3
"""Lädt eine Audiodatei zu einem POI hoch."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, ask_path, client_from_args, run
parser = argparse.ArgumentParser(description="Audiodatei zu einem POI hochladen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
parser.add_argument("--file")
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Audiodatei zu einem POI hochladen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
parser.add_argument("--file")
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
route_id = ask("Route-ID", args.route_id, required=True, cast=int)
poi_id = ask("POI-ID", args.poi_id, required=True, cast=int)
audio = ask_path("Audiodatei", args.file)
return client.request("POST", f"/api/routes/{route_id}/pois/{poi_id}/audio", files={"audio": audio})
run(action)
if __name__ == "__main__":
run(main)
+29 -11
View File
@@ -1,15 +1,25 @@
#!/usr/bin/env python3
"""Lädt ein Bild zu einem POI hoch."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, ask_path, client_from_args, run
parser = argparse.ArgumentParser(description="Bild zu einem POI hochladen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
parser.add_argument("--file")
parser.add_argument("--caption")
parser.add_argument("--sequence", type=int)
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Bild zu einem POI hochladen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
parser.add_argument("--file")
parser.add_argument("--caption")
parser.add_argument("--sequence", type=int)
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
route_id = ask("Route-ID", args.route_id, required=True, cast=int)
poi_id = ask("POI-ID", args.poi_id, required=True, cast=int)
@@ -18,5 +28,13 @@ def action():
"caption": ask("Bildbeschreibung", args.caption),
"sequence": ask("Reihenfolge (leer: automatisch)", args.sequence, cast=int),
}
return client.request("POST", f"/api/routes/{route_id}/pois/{poi_id}/pictures", fields=fields, files={"picture": picture})
run(action)
return client.request(
"POST",
f"/api/routes/{route_id}/pois/{poi_id}/pictures",
fields=fields,
files={"picture": picture},
)
if __name__ == "__main__":
run(main)
+27 -11
View File
@@ -1,15 +1,27 @@
#!/usr/bin/env python3
"""Legt einen POI für eine Route an."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, client_from_args, run
parser = argparse.ArgumentParser(description="POI anlegen")
add_common_arguments(parser)
for name, kwargs in [
("--route-id", {"type": int}), ("--title", {}), ("--description", {}),
("--lat", {"type": float}), ("--lon", {"type": float}),
("--trigger-radius", {"type": float}), ("--sequence", {"type": int})
]: parser.add_argument(name, **kwargs)
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="POI anlegen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--title")
parser.add_argument("--description")
parser.add_argument("--lat", type=float)
parser.add_argument("--lon", type=float)
parser.add_argument("--trigger-radius", type=float)
parser.add_argument("--sequence", type=int)
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
route_id = ask("Route-ID", args.route_id, required=True, cast=int)
body = {
@@ -20,5 +32,9 @@ def action():
"triggerRadiusM": ask("Auslöseradius in Metern", args.trigger_radius, cast=float),
"sequence": ask("Reihenfolge", args.sequence, cast=int),
}
return client.request("POST", f"/api/routes/{route_id}/pois", json_body={k:v for k,v in body.items() if v is not None})
run(action)
payload = {key: value for key, value in body.items() if value is not None}
return client.request("POST", f"/api/routes/{route_id}/pois", json_body=payload)
if __name__ == "__main__":
run(main)
+21 -10
View File
@@ -1,17 +1,25 @@
#!/usr/bin/env python3
"""Legt eine Route aus einer GPX-Datei an."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, ask_path, client_from_args, run
parser = argparse.ArgumentParser(description="Neue Wegwichtel-Route anlegen")
add_common_arguments(parser)
parser.add_argument("--gpx")
parser.add_argument("--name")
parser.add_argument("--slug")
parser.add_argument("--description")
parser.add_argument("--school-name")
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Neue Wegwichtel-Route anlegen")
add_common_arguments(parser)
parser.add_argument("--gpx")
parser.add_argument("--name")
parser.add_argument("--slug")
parser.add_argument("--description")
parser.add_argument("--school-name")
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
gpx = ask_path("GPX-Datei", args.gpx)
fields = {
@@ -21,4 +29,7 @@ def action():
"schoolName": ask("Schule", args.school_name),
}
return client.request("POST", "/api/routes", fields=fields, files={"gpx": gpx})
run(action)
if __name__ == "__main__":
run(main)
+20 -7
View File
@@ -1,16 +1,29 @@
#!/usr/bin/env python3
"""Löscht die Audiodatei eines POIs."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, client_from_args, confirm, run
parser = argparse.ArgumentParser(description="Audiodatei eines POIs löschen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Audiodatei eines POIs löschen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
route_id = ask("Route-ID", args.route_id, required=True, cast=int)
poi_id = ask("POI-ID", args.poi_id, required=True, cast=int)
if not confirm(f"Audio für POI {poi_id} löschen?"):
return {"cancelled": True}
return client.request("DELETE", f"/api/routes/{route_id}/pois/{poi_id}/audio")
run(action)
if __name__ == "__main__":
run(main)
+21 -8
View File
@@ -1,13 +1,23 @@
#!/usr/bin/env python3
"""Löscht ein Bild eines POIs."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, client_from_args, confirm, run
parser = argparse.ArgumentParser(description="Ein Bild löschen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
parser.add_argument("--picture-id", type=int)
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Ein Bild löschen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
parser.add_argument("--picture-id", type=int)
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
route_id = ask("Route-ID", args.route_id, required=True, cast=int)
poi_id = ask("POI-ID", args.poi_id, required=True, cast=int)
@@ -15,4 +25,7 @@ def action():
if not confirm(f"Bild {picture_id} löschen?"):
return {"cancelled": True}
return client.request("DELETE", f"/api/routes/{route_id}/pois/{poi_id}/pictures/{picture_id}")
run(action)
if __name__ == "__main__":
run(main)
+20 -7
View File
@@ -1,16 +1,29 @@
#!/usr/bin/env python3
"""Löscht einen POI einschließlich seiner Medien."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, client_from_args, confirm, run
parser = argparse.ArgumentParser(description="POI einschließlich Medien löschen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="POI einschließlich Medien löschen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
route_id = ask("Route-ID", args.route_id, required=True, cast=int)
poi_id = ask("POI-ID", args.poi_id, required=True, cast=int)
if not confirm(f"POI {poi_id} einschließlich Bilder und Audio löschen?"):
return {"cancelled": True}
return client.request("DELETE", f"/api/routes/{route_id}/pois/{poi_id}")
run(action)
if __name__ == "__main__":
run(main)
+19 -6
View File
@@ -1,14 +1,27 @@
#!/usr/bin/env python3
"""Verschiebt eine Route in den Papierkorb."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, client_from_args, confirm, run
parser = argparse.ArgumentParser(description="Route weich löschen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Route weich löschen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
route_id = ask("Route-ID", args.route_id, required=True, cast=int)
if not confirm(f"Route {route_id} in den Papierkorb verschieben?"):
return {"cancelled": True}
return client.request("DELETE", f"/api/routes/{route_id}")
run(action)
if __name__ == "__main__":
run(main)
+19 -6
View File
@@ -1,12 +1,25 @@
#!/usr/bin/env python3
"""Stellt eine gelöschte Route wieder her."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, client_from_args, run
parser = argparse.ArgumentParser(description="Gelöschte Route wiederherstellen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Gelöschte Route wiederherstellen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
route_id = ask("Route-ID", args.route_id, required=True, cast=int)
return client.request("POST", f"/api/routes/{route_id}/restore", json_body={})
run(action)
if __name__ == "__main__":
run(main)
+21 -8
View File
@@ -1,16 +1,29 @@
#!/usr/bin/env python3
"""Ersetzt die Audiodatei eines POIs."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, ask_path, client_from_args, run
parser = argparse.ArgumentParser(description="Audiodatei eines POIs ersetzen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
parser.add_argument("--file")
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Audiodatei eines POIs ersetzen")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
parser.add_argument("--file")
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
route_id = ask("Route-ID", args.route_id, required=True, cast=int)
poi_id = ask("POI-ID", args.poi_id, required=True, cast=int)
audio = ask_path("Neue Audiodatei", args.file)
return client.request("PUT", f"/api/routes/{route_id}/pois/{poi_id}/audio", files={"audio": audio})
run(action)
if __name__ == "__main__":
run(main)
+24 -11
View File
@@ -1,26 +1,39 @@
#!/usr/bin/env python3
"""Ändert Bildmetadaten und ersetzt optional die Bilddatei."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, ask_path, ask_with_default, client_from_args, run
parser = argparse.ArgumentParser(description="Bilddatei oder Bildmetadaten ändern")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
parser.add_argument("--picture-id", type=int)
parser.add_argument("--file")
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Bilddatei oder Bildmetadaten ändern")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
parser.add_argument("--picture-id", type=int)
parser.add_argument("--file")
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
route_id = ask("Route-ID", args.route_id, required=True, cast=int)
poi_id = ask("POI-ID", args.poi_id, required=True, cast=int)
picture_id = ask("Bild-ID", args.picture_id, required=True, cast=int)
current = client.request("GET", f"/api/routes/{route_id}/pois/{poi_id}/pictures/{picture_id}?metadata=true")
path = f"/api/routes/{route_id}/pois/{poi_id}/pictures/{picture_id}"
current = client.request("GET", f"{path}?metadata=true")
fields = {
"caption": ask_with_default("Bildbeschreibung", current.get("caption", "")),
"sequence": ask_with_default("Reihenfolge", current.get("sequence", 0), int),
}
picture = ask_path("Neue Bilddatei (leer: unverändert)", args.file, required=False)
path = f"/api/routes/{route_id}/pois/{poi_id}/pictures/{picture_id}"
if picture:
return client.request("PUT", path, fields=fields, files={"picture": picture})
return client.request("PUT", path, json_body=fields)
run(action)
if __name__ == "__main__":
run(main)
+23 -9
View File
@@ -1,16 +1,27 @@
#!/usr/bin/env python3
"""Ändert die Metadaten eines POIs."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, ask_with_default, client_from_args, run
parser = argparse.ArgumentParser(description="POI ändern")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="POI ändern")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--poi-id", type=int)
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
route_id = ask("Route-ID", args.route_id, required=True, cast=int)
poi_id = ask("POI-ID", args.poi_id, required=True, cast=int)
current = client.request("GET", f"/api/routes/{route_id}/pois/{poi_id}")
path = f"/api/routes/{route_id}/pois/{poi_id}"
current = client.request("GET", path)
body = {
"title": ask_with_default("Titel", current.get("title", "")),
"description": ask_with_default("Beschreibung", current.get("description", "")),
@@ -19,5 +30,8 @@ def action():
"triggerRadiusM": ask_with_default("Auslöseradius in Metern", current.get("triggerRadiusM"), float),
"sequence": ask_with_default("Reihenfolge", current.get("sequence"), int),
}
return client.request("PUT", f"/api/routes/{route_id}/pois/{poi_id}", json_body=body)
run(action)
return client.request("PUT", path, json_body=body)
if __name__ == "__main__":
run(main)
+22 -10
View File
@@ -1,17 +1,26 @@
#!/usr/bin/env python3
"""Ändert Routendaten und ersetzt optional die GPX-Datei."""
import argparse
from collections.abc import Sequence
from common import add_common_arguments, ask, ask_path, ask_with_default, client_from_args, run
parser = argparse.ArgumentParser(description="Wegwichtel-Route ändern")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--gpx")
args = parser.parse_args()
def action():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Wegwichtel-Route ändern")
add_common_arguments(parser)
parser.add_argument("--route-id", type=int)
parser.add_argument("--gpx")
return parser
def main(argv: Sequence[str] | None = None):
args = build_parser().parse_args(argv)
client = client_from_args(args)
route_id = ask("Route-ID", args.route_id, required=True, cast=int)
current = client.request("GET", f"/api/routes/{route_id}")
path = f"/api/routes/{route_id}"
current = client.request("GET", path)
fields = {
"name": ask_with_default("Name", current.get("name", "")),
"description": ask_with_default("Beschreibung", current.get("description", "")),
@@ -19,6 +28,9 @@ def action():
}
gpx = ask_path("Neue GPX-Datei (leer: unverändert)", args.gpx, required=False)
if gpx:
return client.request("PUT", f"/api/routes/{route_id}", fields=fields, files={"gpx": gpx})
return client.request("PUT", f"/api/routes/{route_id}", json_body=fields)
run(action)
return client.request("PUT", path, fields=fields, files={"gpx": gpx})
return client.request("PUT", path, json_body=fields)
if __name__ == "__main__":
run(main)