first commit
This commit is contained in:
+162
@@ -0,0 +1,162 @@
|
||||
"""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] + "…"
|
||||
Reference in New Issue
Block a user