# -*- coding: utf-8 -*- """ Time Conditions - Zeitbasierte Bedingungen. """ from datetime import datetime, time, date from typing import Any from trixy_core.scheduler.condition.base import Condition from trixy_core.scheduler.form_fields import FieldType, FormField, FormValidationError class TimeRangeCondition(Condition): """ Prüft ob aktuelle Zeit in einem Zeitfenster liegt. Beispiel: - TimeRangeCondition(time(9, 0), time(17, 0)) - 9:00-17:00 - TimeRangeCondition(time(22, 0), time(6, 0)) - 22:00-06:00 (über Mitternacht) """ def __init__( self, start_time: time, end_time: time, name: str | None = None, invert: bool = False, ): """ Initialisiert die Condition. Args: start_time: Startzeit end_time: Endzeit name: Condition-Name invert: Ergebnis invertieren """ super().__init__(name, invert) self._start_time = start_time self._end_time = end_time @property def start_time(self) -> time: """Startzeit.""" return self._start_time @property def end_time(self) -> time: """Endzeit.""" return self._end_time def _evaluate(self, context: dict) -> bool: """Prüft ob aktuelle Zeit im Zeitfenster liegt.""" now = context.get("current_time", datetime.now()) if isinstance(now, datetime): current_time = now.time() else: current_time = now # Normal: Start < End (z.B. 9:00-17:00) if self._start_time <= self._end_time: return self._start_time <= current_time <= self._end_time # Über Mitternacht: Start > End (z.B. 22:00-06:00) return current_time >= self._start_time or current_time <= self._end_time def to_dict(self) -> dict: """Konvertiert zu Dictionary.""" return { **self._base_dict(), "start_time": self._start_time.isoformat(), "end_time": self._end_time.isoformat(), } @classmethod def from_dict(cls, data: dict) -> "TimeRangeCondition": """Erstellt Condition aus Dictionary.""" return cls( start_time=time.fromisoformat(data["start_time"]), end_time=time.fromisoformat(data["end_time"]), name=data.get("name"), invert=data.get("invert", False), ) @classmethod def display_name(cls) -> str: """Anzeigename fuer die TUI.""" return "Zeitfenster" @classmethod def form_fields(cls) -> list[FormField]: """Gibt die Formular-Felder fuer die TUI zurueck.""" return [ FormField( name="start_time", label="Startzeit", required=True, placeholder="09:00:00", help_text="Format: HH:MM:SS", ), FormField( name="end_time", label="Endzeit", required=True, placeholder="17:00:00", help_text="Format: HH:MM:SS", ), FormField( name="invert", label="Invertieren", field_type=FieldType.CHECKBOX, default=False, ), ] @classmethod def validate_form(cls, data: dict) -> list[FormValidationError]: """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck.""" errors: list[FormValidationError] = [] for field_name in ("start_time", "end_time"): val = data.get(field_name, "") if not val: label = "Startzeit" if field_name == "start_time" else "Endzeit" errors.append(FormValidationError(field_name, f"{label} ist erforderlich")) else: try: time.fromisoformat(val) except ValueError: errors.append(FormValidationError(field_name, "Ungueltiges Zeitformat (HH:MM:SS erwartet)")) return errors class DayOfWeekCondition(Condition): """ Prüft ob aktueller Wochentag in Liste ist. Beispiel: - DayOfWeekCondition([0, 1, 2, 3, 4]) - Montag-Freitag - DayOfWeekCondition([5, 6]) - Wochenende """ # Aliase für Wochentage (Python: 0=Mo, 6=So) DAY_ALIASES = { "MON": 0, "MONDAY": 0, "TUE": 1, "TUESDAY": 1, "WED": 2, "WEDNESDAY": 2, "THU": 3, "THURSDAY": 3, "FRI": 4, "FRIDAY": 4, "SAT": 5, "SATURDAY": 5, "SUN": 6, "SUNDAY": 6, } def __init__( self, days: list[int | str], name: str | None = None, invert: bool = False, ): """ Initialisiert die Condition. Args: days: Liste von Wochentagen (0-6 oder Namen) name: Condition-Name invert: Ergebnis invertieren """ super().__init__(name, invert) self._days = self._parse_days(days) def _parse_days(self, days: list[int | str]) -> set[int]: """Parsed Wochentage.""" result = set() for day in days: if isinstance(day, int): if 0 <= day <= 6: result.add(day) elif isinstance(day, str): day_upper = day.upper() if day_upper in self.DAY_ALIASES: result.add(self.DAY_ALIASES[day_upper]) return result @property def days(self) -> set[int]: """Erlaubte Wochentage.""" return self._days.copy() def _evaluate(self, context: dict) -> bool: """Prüft ob aktueller Wochentag erlaubt ist.""" now = context.get("current_time", datetime.now()) if isinstance(now, datetime): weekday = now.weekday() else: weekday = datetime.now().weekday() return weekday in self._days def to_dict(self) -> dict: """Konvertiert zu Dictionary.""" return { **self._base_dict(), "days": list(self._days), } @classmethod def from_dict(cls, data: dict) -> "DayOfWeekCondition": """Erstellt Condition aus Dictionary.""" return cls( days=data["days"], name=data.get("name"), invert=data.get("invert", False), ) @classmethod def display_name(cls) -> str: """Anzeigename fuer die TUI.""" return "Wochentag" @classmethod def form_fields(cls) -> list[FormField]: """Gibt die Formular-Felder fuer die TUI zurueck.""" return [ FormField( name="days", label="Wochentage", field_type=FieldType.WEEKDAYS, required=True, help_text="Erlaubte Wochentage", ), FormField( name="invert", label="Invertieren", field_type=FieldType.CHECKBOX, default=False, ), ] @classmethod def validate_form(cls, data: dict) -> list[FormValidationError]: """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck.""" errors: list[FormValidationError] = [] days = data.get("days", []) if not days: errors.append(FormValidationError("days", "Mindestens ein Wochentag muss ausgewaehlt sein")) return errors @classmethod def weekdays(cls, name: str | None = None) -> "DayOfWeekCondition": """Erstellt Condition für Werktage (Mo-Fr).""" return cls([0, 1, 2, 3, 4], name=name or "Weekdays") @classmethod def weekends(cls, name: str | None = None) -> "DayOfWeekCondition": """Erstellt Condition für Wochenenden (Sa-So).""" return cls([5, 6], name=name or "Weekends") class DateRangeCondition(Condition): """ Prüft ob aktuelles Datum in einem Bereich liegt. Beispiel: - DateRangeCondition(date(2024, 12, 1), date(2024, 12, 31)) - Dezember 2024 """ def __init__( self, start_date: date | None = None, end_date: date | None = None, name: str | None = None, invert: bool = False, ): """ Initialisiert die Condition. Args: start_date: Startdatum (None = unbegrenzt) end_date: Enddatum (None = unbegrenzt) name: Condition-Name invert: Ergebnis invertieren """ super().__init__(name, invert) self._start_date = start_date self._end_date = end_date @property def start_date(self) -> date | None: """Startdatum.""" return self._start_date @property def end_date(self) -> date | None: """Enddatum.""" return self._end_date def _evaluate(self, context: dict) -> bool: """Prüft ob aktuelles Datum im Bereich liegt.""" now = context.get("current_time", datetime.now()) if isinstance(now, datetime): current_date = now.date() elif isinstance(now, date): current_date = now else: current_date = datetime.now().date() if self._start_date and current_date < self._start_date: return False if self._end_date and current_date > self._end_date: return False return True def to_dict(self) -> dict: """Konvertiert zu Dictionary.""" return { **self._base_dict(), "start_date": self._start_date.isoformat() if self._start_date else None, "end_date": self._end_date.isoformat() if self._end_date else None, } @classmethod def from_dict(cls, data: dict) -> "DateRangeCondition": """Erstellt Condition aus Dictionary.""" start = None end = None if data.get("start_date"): start = date.fromisoformat(data["start_date"]) if data.get("end_date"): end = date.fromisoformat(data["end_date"]) return cls( start_date=start, end_date=end, name=data.get("name"), invert=data.get("invert", False), ) @classmethod def display_name(cls) -> str: """Anzeigename fuer die TUI.""" return "Datumsbereich" @classmethod def form_fields(cls) -> list[FormField]: """Gibt die Formular-Felder fuer die TUI zurueck.""" return [ FormField( name="start_date", label="Startdatum", field_type=FieldType.DATETIME, placeholder="2024-01-01", help_text="Format: YYYY-MM-DD", ), FormField( name="end_date", label="Enddatum", field_type=FieldType.DATETIME, placeholder="2024-12-31", help_text="Format: YYYY-MM-DD", ), FormField( name="invert", label="Invertieren", field_type=FieldType.CHECKBOX, default=False, ), ] @classmethod def validate_form(cls, data: dict) -> list[FormValidationError]: """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck.""" errors: list[FormValidationError] = [] for fn in ("start_date", "end_date"): val = data.get(fn, "") if val: try: date.fromisoformat(val) except ValueError: label = "Startdatum" if fn == "start_date" else "Enddatum" errors.append(FormValidationError(fn, f"Ungueltiges {label} (YYYY-MM-DD erwartet)")) return errors