included scripts for api interaction
This commit is contained in:
parent
a697e293cb
commit
3b3069ca8c
67
tools/python/README.md
Normal file
67
tools/python/README.md
Normal file
@ -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.
|
||||||
14
tools/python/append_route.py
Normal file
14
tools/python/append_route.py
Normal file
@ -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)
|
||||||
241
tools/python/common.py
Normal file
241
tools/python/common.py
Normal file
@ -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
|
||||||
16
tools/python/create_audio.py
Normal file
16
tools/python/create_audio.py
Normal file
@ -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)
|
||||||
22
tools/python/create_picture.py
Normal file
22
tools/python/create_picture.py
Normal file
@ -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)
|
||||||
24
tools/python/create_poi.py
Normal file
24
tools/python/create_poi.py
Normal file
@ -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)
|
||||||
24
tools/python/create_route.py
Normal file
24
tools/python/create_route.py
Normal file
@ -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)
|
||||||
16
tools/python/delete_audio.py
Normal file
16
tools/python/delete_audio.py
Normal file
@ -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)
|
||||||
18
tools/python/delete_picture.py
Normal file
18
tools/python/delete_picture.py
Normal file
@ -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)
|
||||||
16
tools/python/delete_poi.py
Normal file
16
tools/python/delete_poi.py
Normal file
@ -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)
|
||||||
14
tools/python/delete_route.py
Normal file
14
tools/python/delete_route.py
Normal file
@ -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)
|
||||||
12
tools/python/restore_route.py
Normal file
12
tools/python/restore_route.py
Normal file
@ -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)
|
||||||
16
tools/python/update_audio.py
Normal file
16
tools/python/update_audio.py
Normal file
@ -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)
|
||||||
26
tools/python/update_picture.py
Normal file
26
tools/python/update_picture.py
Normal file
@ -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)
|
||||||
23
tools/python/update_poi.py
Normal file
23
tools/python/update_poi.py
Normal file
@ -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)
|
||||||
24
tools/python/update_route.py
Normal file
24
tools/python/update_route.py
Normal file
@ -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)
|
||||||
Loading…
x
Reference in New Issue
Block a user