"""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] + "…"