Files
patrickandClaude Sonnet 5.5 5278530467
test / pytest (3.11) (push) Canceled after 0s
test / pytest (3.13) (push) Canceled after 0s
feat: Phase 6b rules with delay, delta format, reminders, zpool default rule
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 02:38:10 +01:00

68 lines
2.8 KiB
Python

import re
from dataclasses import dataclass, field
from ..chat.base import Target
from ..chat.targets import valid_target
from ..globs import validate_glob
from .limits import (AI_HINT_MAX, DEFAULT_MIN_STATE, DELAY_MINUTES_RANGE, MIN_STATE_RANGE, NAME_MAX,
OUTPUT_CONTAINS_MAX, REMINDER_HOURS_RANGE, TARGET_MAX)
from .rules import ALL_KINDS
BACKENDS = ("telegram", "matrix")
CONTROL_RE = re.compile(r"[\x00-\x08\x0b-\x1f\x7f]")
@dataclass(frozen=True)
class RouteInput:
name: str = ""
match_host: str = ""
match_service: str = ""
min_state: int = DEFAULT_MIN_STATE
kinds: tuple[str, ...] = ALL_KINDS
stop_on_match: bool = False
enabled: bool = True
targets: tuple[Target, ...] = field(default_factory=tuple)
delay_minutes: int | None = None
output_contains: str = ""
ai_enabled: bool = True
ai_hint: str = ""
reminder_hours: int | None = None
loud: bool = True
use_default_targets: bool = False
def _range_error(value: int | None, bounds: tuple[int, int], label: str, unit: str = "") -> list[str]:
low, high = bounds
if value is not None and not low <= value <= high:
suffix = f" {unit}" if unit else ""
return [f"{label} muss zwischen {low} und {high}{suffix} liegen."]
return []
def validate_route(data: RouteInput) -> list[str]:
errors = []
if not data.name.strip():
errors.append("Name fehlt.")
elif len(data.name) > NAME_MAX:
errors.append(f"Name ist zu lang (max. {NAME_MAX} Zeichen).")
errors += validate_glob(data.match_host, "Host-Muster") + validate_glob(data.match_service, "Service-Muster")
errors += _range_error(data.min_state, MIN_STATE_RANGE, "Mindest-Schweregrad")
if not data.kinds or any(kind not in ALL_KINDS for kind in data.kinds):
errors.append("Mindestens eine gültige Alarmart wählen.")
errors += _range_error(data.delay_minutes, DELAY_MINUTES_RANGE, "Verzögerung", "Minuten")
errors += _range_error(data.reminder_hours, REMINDER_HOURS_RANGE, "Erinnerung", "Stunden")
if len(data.output_contains) > OUTPUT_CONTAINS_MAX:
errors.append(f"Der Ausgabe-Text ist zu lang (max. {OUTPUT_CONTAINS_MAX} Zeichen).")
if CONTROL_RE.search(data.output_contains):
errors.append("Der Ausgabe-Text enthält Steuerzeichen.")
if len(data.ai_hint) > AI_HINT_MAX:
errors.append(f"Der KI-Hinweis ist zu lang (max. {AI_HINT_MAX} Zeichen).")
if CONTROL_RE.search(data.ai_hint):
errors.append("Der KI-Hinweis enthält Steuerzeichen.")
for target in data.targets:
if target.backend not in BACKENDS:
errors.append(f"Unbekanntes Backend: {target.backend}")
elif len(target.target) > TARGET_MAX or not valid_target(target.backend, target.target):
errors.append(f"Ungültiges Ziel für {target.backend}.")
return errors