163 lines
5.4 KiB
Python
163 lines
5.4 KiB
Python
"""Kleine Hilfsfunktionen fuer pvesnap (Dauer-Parsing, Slugs, Listen)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import unicodedata
|
|
|
|
_DURATION_UNITS = {
|
|
"s": 1, "sec": 1, "secs": 1, "second": 1, "seconds": 1,
|
|
"sek": 1, "sekunde": 1, "sekunden": 1,
|
|
"m": 60, "min": 60, "mins": 60, "minute": 60, "minutes": 60, "minuten": 60,
|
|
"h": 3600, "hour": 3600, "hours": 3600, "std": 3600, "stunde": 3600, "stunden": 3600,
|
|
"d": 86400, "day": 86400, "days": 86400, "t": 86400, "tag": 86400, "tage": 86400,
|
|
"w": 604800, "week": 604800, "weeks": 604800, "woche": 604800, "wochen": 604800,
|
|
"mo": 2592000, "month": 2592000, "months": 2592000, "monat": 2592000, "monate": 2592000,
|
|
"y": 31536000, "year": 31536000, "years": 31536000,
|
|
"j": 31536000, "jahr": 31536000, "jahre": 31536000,
|
|
}
|
|
|
|
_DURATION_TOKEN = re.compile(r"(\d+(?:[.,]\d+)?)\s*([a-zäöü]*)")
|
|
|
|
_NEVER = {"0", "never", "nie", "unbegrenzt", "unlimited", "off", "aus", "keine", "-"}
|
|
|
|
|
|
def parse_duration(text, default_unit="s"):
|
|
"""'2d12h' -> 216000. 0 / 'nie' / 'unbegrenzt' -> 0 (= keine Begrenzung)."""
|
|
if text is None:
|
|
raise ValueError("leere Zeitangabe")
|
|
s = str(text).strip().lower()
|
|
if not s:
|
|
raise ValueError("leere Zeitangabe")
|
|
if s in _NEVER:
|
|
return 0
|
|
|
|
total = 0.0
|
|
pos = 0
|
|
found = False
|
|
for match in _DURATION_TOKEN.finditer(s):
|
|
if match.start() != pos:
|
|
raise ValueError("ungueltige Zeitangabe: %r" % text)
|
|
pos = match.end()
|
|
value = float(match.group(1).replace(",", "."))
|
|
unit = match.group(2) or default_unit
|
|
if unit not in _DURATION_UNITS:
|
|
raise ValueError("unbekannte Zeiteinheit %r in %r" % (unit, text))
|
|
total += value * _DURATION_UNITS[unit]
|
|
found = True
|
|
|
|
if not found or pos != len(s):
|
|
raise ValueError("ungueltige Zeitangabe: %r" % text)
|
|
return int(round(total))
|
|
|
|
|
|
def format_duration(seconds, zero="unbegrenzt"):
|
|
"""216000 -> '2d12h'. Ergebnis ist wieder von parse_duration lesbar."""
|
|
seconds = int(seconds or 0)
|
|
if seconds <= 0:
|
|
return zero
|
|
parts = []
|
|
for unit, size in (("w", 604800), ("d", 86400), ("h", 3600), ("m", 60), ("s", 1)):
|
|
if seconds >= size:
|
|
count, seconds = divmod(seconds, size)
|
|
parts.append("%d%s" % (count, unit))
|
|
return "".join(parts)
|
|
|
|
|
|
def slugify(text, maxlen=20):
|
|
"""Gruppenname -> kurzer, PVE-tauglicher Bezeichner (nur a-z0-9, beginnt mit Buchstabe)."""
|
|
# Umlaute vor der Normalisierung ersetzen - NFKD wuerde sie sonst zerlegen.
|
|
lowered = str(text).lower()
|
|
for umlaut, replacement in (("ä", "ae"), ("ö", "oe"), ("ü", "ue"), ("ß", "ss")):
|
|
lowered = lowered.replace(umlaut, replacement)
|
|
ascii_only = unicodedata.normalize("NFKD", lowered).encode("ascii", "ignore").decode("ascii")
|
|
slug = re.sub(r"[^a-z0-9]+", "", ascii_only)
|
|
if not slug:
|
|
slug = "grp"
|
|
if not slug[0].isalpha():
|
|
slug = "g" + slug
|
|
return slug[:maxlen]
|
|
|
|
|
|
_TRUE = {"1", "yes", "y", "true", "on", "ja", "j", "an", "ein", "enabled", "aktiv"}
|
|
_FALSE = {"0", "no", "n", "false", "off", "nein", "aus", "disabled", "inaktiv", ""}
|
|
|
|
|
|
def parse_bool(text, default=None):
|
|
if isinstance(text, bool):
|
|
return text
|
|
value = str(text).strip().lower()
|
|
if value in _TRUE:
|
|
return True
|
|
if value in _FALSE:
|
|
return default if (value == "" and default is not None) else False
|
|
raise ValueError("ungueltiger Ja/Nein-Wert: %r" % text)
|
|
|
|
|
|
def format_bool(value):
|
|
return "yes" if value else "no"
|
|
|
|
|
|
def parse_list(text):
|
|
"""Kommagetrennte (oder per Semikolon/Whitespace getrennte) Liste -> Liste von Strings."""
|
|
if not text:
|
|
return []
|
|
if isinstance(text, (list, tuple)):
|
|
items = list(text)
|
|
else:
|
|
items = re.split(r"[,;\s]+", str(text))
|
|
return [item.strip() for item in items if item and item.strip()]
|
|
|
|
|
|
def parse_vmid_list(text):
|
|
"""'100,101,105-108' -> [100, 101, 105, 106, 107, 108]."""
|
|
result = []
|
|
for token in parse_list(text):
|
|
match = re.match(r"^(\d+)\s*-\s*(\d+)$", token)
|
|
if match:
|
|
start, end = int(match.group(1)), int(match.group(2))
|
|
if end < start:
|
|
start, end = end, start
|
|
if end - start > 100000:
|
|
raise ValueError("VMID-Bereich zu gross: %r" % token)
|
|
result.extend(range(start, end + 1))
|
|
elif token.isdigit():
|
|
result.append(int(token))
|
|
else:
|
|
raise ValueError("ungueltige VMID: %r" % token)
|
|
return sorted(set(result))
|
|
|
|
|
|
def format_vmid_list(vmids):
|
|
"""[100,101,102,105] -> '100-102,105' (Umkehrung von parse_vmid_list)."""
|
|
ids = sorted(set(int(v) for v in vmids))
|
|
if not ids:
|
|
return ""
|
|
chunks = []
|
|
start = previous = ids[0]
|
|
for vmid in ids[1:]:
|
|
if vmid == previous + 1:
|
|
previous = vmid
|
|
continue
|
|
chunks.append((start, previous))
|
|
start = previous = vmid
|
|
chunks.append((start, previous))
|
|
out = []
|
|
for begin, end in chunks:
|
|
if end - begin >= 2:
|
|
out.append("%d-%d" % (begin, end))
|
|
else:
|
|
out.extend(str(v) for v in range(begin, end + 1))
|
|
return ",".join(out)
|
|
|
|
|
|
def truncate(text, width):
|
|
text = str(text)
|
|
if width <= 0:
|
|
return ""
|
|
if len(text) <= width:
|
|
return text
|
|
if width <= 1:
|
|
return text[:width]
|
|
return text[: width - 1] + "…"
|