From 3b3069ca8ce2bd2e3f82fca96224aafec35126c8 Mon Sep 17 00:00:00 2001 From: Florian Zumpe Date: Wed, 17 Jun 2026 11:41:36 +0200 Subject: [PATCH] included scripts for api interaction --- tools/python/README.md | 67 +++++++++ tools/python/append_route.py | 14 ++ tools/python/common.py | 241 +++++++++++++++++++++++++++++++++ tools/python/create_audio.py | 16 +++ tools/python/create_picture.py | 22 +++ tools/python/create_poi.py | 24 ++++ tools/python/create_route.py | 24 ++++ tools/python/delete_audio.py | 16 +++ tools/python/delete_picture.py | 18 +++ tools/python/delete_poi.py | 16 +++ tools/python/delete_route.py | 14 ++ tools/python/restore_route.py | 12 ++ tools/python/update_audio.py | 16 +++ tools/python/update_picture.py | 26 ++++ tools/python/update_poi.py | 23 ++++ tools/python/update_route.py | 24 ++++ 16 files changed, 573 insertions(+) create mode 100644 tools/python/README.md create mode 100644 tools/python/append_route.py create mode 100644 tools/python/common.py create mode 100644 tools/python/create_audio.py create mode 100644 tools/python/create_picture.py create mode 100644 tools/python/create_poi.py create mode 100644 tools/python/create_route.py create mode 100644 tools/python/delete_audio.py create mode 100644 tools/python/delete_picture.py create mode 100644 tools/python/delete_poi.py create mode 100644 tools/python/delete_route.py create mode 100644 tools/python/restore_route.py create mode 100644 tools/python/update_audio.py create mode 100644 tools/python/update_picture.py create mode 100644 tools/python/update_poi.py create mode 100644 tools/python/update_route.py diff --git a/tools/python/README.md b/tools/python/README.md new file mode 100644 index 0000000..2eaee0e --- /dev/null +++ b/tools/python/README.md @@ -0,0 +1,67 @@ +# 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. + +## Authentifizierung + +Jeder Request wird zunächst mit den bereits bekannten Zugangsdaten oder ohne Zugangsdaten ausgeführt. Antwortet ein vorgeschalteter Webserver mit `401 Unauthorized`, fragt das Werkzeug Benutzername und Passwort interaktiv ab und wiederholt denselben Request. Das Passwort wird mit `getpass` ohne Bildschirmausgabe eingelesen. + +Optionale Umgebungsvariablen: + +```bash +export WEGWICHTEL_URL='https://wegwichtel.de' +export WEGWICHTEL_USER='florian' +# WEGWICHTEL_PASSWORD nur für unbeaufsichtigte Aufrufe verwenden. +``` + +Alle Skripte unterstützen außerdem `--base-url` und `--username`. Fehlende fachliche Parameter werden interaktiv abgefragt. + +## Routen + +```bash +python3 tools/python/create_route.py +python3 tools/python/update_route.py +python3 tools/python/append_route.py +python3 tools/python/delete_route.py +python3 tools/python/restore_route.py +``` + +## POIs + +```bash +python3 tools/python/create_poi.py +python3 tools/python/update_poi.py +python3 tools/python/delete_poi.py +``` + +Beim Löschen eines POIs entfernt die API auch dessen Bilder und Audiodatei. + +## Bilder + +```bash +python3 tools/python/create_picture.py +python3 tools/python/update_picture.py +python3 tools/python/delete_picture.py +``` + +## Audio + +```bash +python3 tools/python/create_audio.py +python3 tools/python/update_audio.py +python3 tools/python/delete_audio.py +``` + +## Beispiel mit Parametern + +```bash +python3 tools/python/create_picture.py \ + --base-url https://wegwichtel.de \ + --route-id 1 \ + --poi-id 7 \ + --file ./eiche.jpg \ + --caption 'Blick auf die Baumkrone' \ + --sequence 0 +``` + +Die Multipart-Hilfsfunktion sendet für Dateien absichtlich keinen behaupteten MIME-Typ. Der Server bestimmt das Format aus dem Dateiinhalt. diff --git a/tools/python/append_route.py b/tools/python/append_route.py new file mode 100644 index 0000000..1e25313 --- /dev/null +++ b/tools/python/append_route.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +import argparse +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(): + 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) diff --git a/tools/python/common.py b/tools/python/common.py new file mode 100644 index 0000000..72d908b --- /dev/null +++ b/tools/python/common.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +"""Gemeinsame Funktionen für die Wegwichtel-API-Werkzeuge.""" + +from __future__ import annotations + +import argparse +import base64 +import getpass +import json +import os +import sys +import uuid +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable +from urllib.error import HTTPError, URLError +from urllib.parse import urljoin +from urllib.request import Request, urlopen + +DEFAULT_BASE_URL = "https://wegwichtel.de" + + +class ApiError(RuntimeError): + def __init__(self, status: int, message: str, payload: Any = None): + super().__init__(f"HTTP {status}: {message}") + self.status = status + self.payload = payload + + +@dataclass +class Credentials: + username: str + password: str + + +class ApiClient: + """Kleine HTTP-Schicht mit dynamischer Basic-Auth-Nachfrage bei 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 + + def prompt_credentials(self) -> None: + default_user = self.credentials.username if self.credentials else os.getenv("WEGWICHTEL_USER", "") + username = input(f"Benutzername{f' [{default_user}]' if default_user else ''}: ").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: ") + 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") + return f"Basic {token}" + + def request( + self, + method: str, + path: str, + *, + json_body: dict[str, Any] | None = None, + fields: dict[str, Any] | None = None, + 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" + + 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()) + 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", "")) + 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 + continue + 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 + + +def encode_multipart(fields: dict[str, Any], files: dict[str, Path]) -> tuple[bytes, str]: + """Erzeugt multipart/form-data; Dateitypen werden absichtlich nicht mitgesendet.""" + boundary = f"----wegwichtel-{uuid.uuid4().hex}" + chunks: list[bytes] = [] + + for name, value in fields.items(): + if value is None: + continue + chunks.extend([ + f"--{boundary}\r\n".encode(), + f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode(), + str(value).encode("utf-8"), + b"\r\n", + ]) + + for name, file_path in files.items(): + path = Path(file_path).expanduser().resolve() + if not path.is_file(): + raise FileNotFoundError(f"Datei nicht gefunden: {path}") + safe_name = path.name.replace('"', "_") + chunks.extend([ + f"--{boundary}\r\n".encode(), + f'Content-Disposition: form-data; name="{name}"; filename="{safe_name}"\r\n\r\n'.encode(), + path.read_bytes(), + b"\r\n", + ]) + + chunks.append(f"--{boundary}--\r\n".encode()) + return b"".join(chunks), f"multipart/form-data; boundary={boundary}" + + +def parse_json(raw: bytes) -> Any: + if not raw: + return None + try: + return json.loads(raw.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError): + return None + + +def parse_response(raw: bytes, content_type: str) -> Any: + if not raw: + return None + if "json" in content_type.lower(): + parsed = parse_json(raw) + if parsed is not None: + return parsed + return raw.decode("utf-8", errors="replace") + + +def decode_error(raw: bytes) -> str: + parsed = parse_json(raw) + if isinstance(parsed, dict): + return str(parsed.get("message") or parsed.get("error") or parsed) + text = raw.decode("utf-8", errors="replace").strip() + return text or "Unbekannter API-Fehler" + + +def add_common_arguments(parser: argparse.ArgumentParser) -> None: + parser.add_argument("--base-url", help="API-Basisadresse, Standard: WEGWICHTEL_URL oder https://wegwichtel.de") + parser.add_argument("--username", help="Benutzername; Passwort wird bei HTTP 401 sicher abgefragt") + + +def client_from_args(args: argparse.Namespace) -> ApiClient: + base_url = args.base_url or os.getenv("WEGWICHTEL_URL") or DEFAULT_BASE_URL + username = args.username or os.getenv("WEGWICHTEL_USER") + password = os.getenv("WEGWICHTEL_PASSWORD") if username else None + return ApiClient(base_url, username, password) + + +def ask(label: str, value: Any = None, *, required: bool = False, cast: Callable[[str], Any] = str) -> Any: + if value is not None: + return cast(str(value)) + while True: + raw = input(f"{label}: ").strip() + if raw: + try: + return cast(raw) + except (TypeError, ValueError) as error: + print(f"Ungültiger Wert: {error}", file=sys.stderr) + continue + if not required: + return None + print("Dieses Feld ist erforderlich.", file=sys.stderr) + + +def ask_with_default(label: str, current: Any, cast: Callable[[str], Any] = str) -> Any: + shown = "" if current is None else str(current) + while True: + raw = input(f"{label} [{shown}]: ").strip() + if not raw: + return current + try: + return cast(raw) + except (TypeError, ValueError) as error: + print(f"Ungültiger Wert: {error}", file=sys.stderr) + + +def ask_path(label: str, value: str | None = None, *, required: bool = True) -> Path | None: + if value is not None: + path = Path(value).expanduser().resolve() + if not path.is_file(): + raise FileNotFoundError(f"Datei nicht gefunden: {path}") + return path + + while True: + raw = input(f"{label}: ").strip() + if not raw and not required: + return None + if not raw: + print("Dieses Feld ist erforderlich.", file=sys.stderr) + continue + path = Path(raw).expanduser().resolve() + if path.is_file(): + return path + print(f"Datei nicht gefunden: {path}", file=sys.stderr) + + +def confirm(question: str) -> bool: + return input(f"{question} [j/N]: ").strip().lower() in {"j", "ja", "y", "yes"} + + +def print_result(result: Any) -> None: + print(json.dumps(result, ensure_ascii=False, indent=2) if not isinstance(result, str) else result) + + +def run(action: Callable[[], Any]) -> None: + try: + print_result(action()) + except (ApiError, RuntimeError, FileNotFoundError, ValueError) as error: + print(f"Fehler: {error}", file=sys.stderr) + raise SystemExit(1) from error diff --git a/tools/python/create_audio.py b/tools/python/create_audio.py new file mode 100644 index 0000000..b87cfef --- /dev/null +++ b/tools/python/create_audio.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +import argparse +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(): + 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) diff --git a/tools/python/create_picture.py b/tools/python/create_picture.py new file mode 100644 index 0000000..bbadeef --- /dev/null +++ b/tools/python/create_picture.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +import argparse +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(): + 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 = ask_path("Bilddatei", args.file) + fields = { + "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) diff --git a/tools/python/create_poi.py b/tools/python/create_poi.py new file mode 100644 index 0000000..10b876f --- /dev/null +++ b/tools/python/create_poi.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import argparse +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(): + client = client_from_args(args) + route_id = ask("Route-ID", args.route_id, required=True, cast=int) + body = { + "title": ask("Titel", args.title), + "description": ask("Beschreibung", args.description), + "lat": ask("Breitengrad", args.lat, required=True, cast=float), + "lon": ask("Längengrad", args.lon, required=True, cast=float), + "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) diff --git a/tools/python/create_route.py b/tools/python/create_route.py new file mode 100644 index 0000000..b40ab23 --- /dev/null +++ b/tools/python/create_route.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import argparse +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(): + client = client_from_args(args) + gpx = ask_path("GPX-Datei", args.gpx) + fields = { + "name": ask("Name (leer: GPX-Name)", args.name), + "slug": ask("Slug (leer: automatisch)", args.slug), + "description": ask("Beschreibung", args.description), + "schoolName": ask("Schule", args.school_name), + } + return client.request("POST", "/api/routes", fields=fields, files={"gpx": gpx}) +run(action) diff --git a/tools/python/delete_audio.py b/tools/python/delete_audio.py new file mode 100644 index 0000000..39b6815 --- /dev/null +++ b/tools/python/delete_audio.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +import argparse +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(): + 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) diff --git a/tools/python/delete_picture.py b/tools/python/delete_picture.py new file mode 100644 index 0000000..9b5baa2 --- /dev/null +++ b/tools/python/delete_picture.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +import argparse +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(): + 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) + 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) diff --git a/tools/python/delete_poi.py b/tools/python/delete_poi.py new file mode 100644 index 0000000..329b0b1 --- /dev/null +++ b/tools/python/delete_poi.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +import argparse +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(): + 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) diff --git a/tools/python/delete_route.py b/tools/python/delete_route.py new file mode 100644 index 0000000..5c768e1 --- /dev/null +++ b/tools/python/delete_route.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +import argparse +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(): + 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) diff --git a/tools/python/restore_route.py b/tools/python/restore_route.py new file mode 100644 index 0000000..8262dde --- /dev/null +++ b/tools/python/restore_route.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +import argparse +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(): + 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) diff --git a/tools/python/update_audio.py b/tools/python/update_audio.py new file mode 100644 index 0000000..6346862 --- /dev/null +++ b/tools/python/update_audio.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +import argparse +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(): + 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) diff --git a/tools/python/update_picture.py b/tools/python/update_picture.py new file mode 100644 index 0000000..835407a --- /dev/null +++ b/tools/python/update_picture.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import argparse +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(): + 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") + 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) diff --git a/tools/python/update_poi.py b/tools/python/update_poi.py new file mode 100644 index 0000000..3f4f82f --- /dev/null +++ b/tools/python/update_poi.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +import argparse +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(): + 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}") + body = { + "title": ask_with_default("Titel", current.get("title", "")), + "description": ask_with_default("Beschreibung", current.get("description", "")), + "lat": ask_with_default("Breitengrad", current.get("lat"), float), + "lon": ask_with_default("Längengrad", current.get("lon"), float), + "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) diff --git a/tools/python/update_route.py b/tools/python/update_route.py new file mode 100644 index 0000000..c93c655 --- /dev/null +++ b/tools/python/update_route.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import argparse +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(): + 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}") + fields = { + "name": ask_with_default("Name", current.get("name", "")), + "description": ask_with_default("Beschreibung", current.get("description", "")), + "schoolName": ask_with_default("Schule", current.get("schoolName", "")), + } + 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)