time.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. # -*- coding: utf-8 -*-
  2. """
  3. Time Conditions - Zeitbasierte Bedingungen.
  4. """
  5. from datetime import datetime, time, date
  6. from typing import Any
  7. from trixy_core.scheduler.condition.base import Condition
  8. from trixy_core.scheduler.form_fields import FieldType, FormField, FormValidationError
  9. class TimeRangeCondition(Condition):
  10. """
  11. Prüft ob aktuelle Zeit in einem Zeitfenster liegt.
  12. Beispiel:
  13. - TimeRangeCondition(time(9, 0), time(17, 0)) - 9:00-17:00
  14. - TimeRangeCondition(time(22, 0), time(6, 0)) - 22:00-06:00 (über Mitternacht)
  15. """
  16. def __init__(
  17. self,
  18. start_time: time,
  19. end_time: time,
  20. name: str | None = None,
  21. invert: bool = False,
  22. ):
  23. """
  24. Initialisiert die Condition.
  25. Args:
  26. start_time: Startzeit
  27. end_time: Endzeit
  28. name: Condition-Name
  29. invert: Ergebnis invertieren
  30. """
  31. super().__init__(name, invert)
  32. self._start_time = start_time
  33. self._end_time = end_time
  34. @property
  35. def start_time(self) -> time:
  36. """Startzeit."""
  37. return self._start_time
  38. @property
  39. def end_time(self) -> time:
  40. """Endzeit."""
  41. return self._end_time
  42. def _evaluate(self, context: dict) -> bool:
  43. """Prüft ob aktuelle Zeit im Zeitfenster liegt."""
  44. now = context.get("current_time", datetime.now())
  45. if isinstance(now, datetime):
  46. current_time = now.time()
  47. else:
  48. current_time = now
  49. # Normal: Start < End (z.B. 9:00-17:00)
  50. if self._start_time <= self._end_time:
  51. return self._start_time <= current_time <= self._end_time
  52. # Über Mitternacht: Start > End (z.B. 22:00-06:00)
  53. return current_time >= self._start_time or current_time <= self._end_time
  54. def to_dict(self) -> dict:
  55. """Konvertiert zu Dictionary."""
  56. return {
  57. **self._base_dict(),
  58. "start_time": self._start_time.isoformat(),
  59. "end_time": self._end_time.isoformat(),
  60. }
  61. @classmethod
  62. def from_dict(cls, data: dict) -> "TimeRangeCondition":
  63. """Erstellt Condition aus Dictionary."""
  64. return cls(
  65. start_time=time.fromisoformat(data["start_time"]),
  66. end_time=time.fromisoformat(data["end_time"]),
  67. name=data.get("name"),
  68. invert=data.get("invert", False),
  69. )
  70. @classmethod
  71. def display_name(cls) -> str:
  72. """Anzeigename fuer die TUI."""
  73. return "Zeitfenster"
  74. @classmethod
  75. def form_fields(cls) -> list[FormField]:
  76. """Gibt die Formular-Felder fuer die TUI zurueck."""
  77. return [
  78. FormField(
  79. name="start_time", label="Startzeit", required=True,
  80. placeholder="09:00:00", help_text="Format: HH:MM:SS",
  81. ),
  82. FormField(
  83. name="end_time", label="Endzeit", required=True,
  84. placeholder="17:00:00", help_text="Format: HH:MM:SS",
  85. ),
  86. FormField(
  87. name="invert", label="Invertieren",
  88. field_type=FieldType.CHECKBOX, default=False,
  89. ),
  90. ]
  91. @classmethod
  92. def validate_form(cls, data: dict) -> list[FormValidationError]:
  93. """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
  94. errors: list[FormValidationError] = []
  95. for field_name in ("start_time", "end_time"):
  96. val = data.get(field_name, "")
  97. if not val:
  98. label = "Startzeit" if field_name == "start_time" else "Endzeit"
  99. errors.append(FormValidationError(field_name, f"{label} ist erforderlich"))
  100. else:
  101. try:
  102. time.fromisoformat(val)
  103. except ValueError:
  104. errors.append(FormValidationError(field_name, "Ungueltiges Zeitformat (HH:MM:SS erwartet)"))
  105. return errors
  106. class DayOfWeekCondition(Condition):
  107. """
  108. Prüft ob aktueller Wochentag in Liste ist.
  109. Beispiel:
  110. - DayOfWeekCondition([0, 1, 2, 3, 4]) - Montag-Freitag
  111. - DayOfWeekCondition([5, 6]) - Wochenende
  112. """
  113. # Aliase für Wochentage (Python: 0=Mo, 6=So)
  114. DAY_ALIASES = {
  115. "MON": 0, "MONDAY": 0,
  116. "TUE": 1, "TUESDAY": 1,
  117. "WED": 2, "WEDNESDAY": 2,
  118. "THU": 3, "THURSDAY": 3,
  119. "FRI": 4, "FRIDAY": 4,
  120. "SAT": 5, "SATURDAY": 5,
  121. "SUN": 6, "SUNDAY": 6,
  122. }
  123. def __init__(
  124. self,
  125. days: list[int | str],
  126. name: str | None = None,
  127. invert: bool = False,
  128. ):
  129. """
  130. Initialisiert die Condition.
  131. Args:
  132. days: Liste von Wochentagen (0-6 oder Namen)
  133. name: Condition-Name
  134. invert: Ergebnis invertieren
  135. """
  136. super().__init__(name, invert)
  137. self._days = self._parse_days(days)
  138. def _parse_days(self, days: list[int | str]) -> set[int]:
  139. """Parsed Wochentage."""
  140. result = set()
  141. for day in days:
  142. if isinstance(day, int):
  143. if 0 <= day <= 6:
  144. result.add(day)
  145. elif isinstance(day, str):
  146. day_upper = day.upper()
  147. if day_upper in self.DAY_ALIASES:
  148. result.add(self.DAY_ALIASES[day_upper])
  149. return result
  150. @property
  151. def days(self) -> set[int]:
  152. """Erlaubte Wochentage."""
  153. return self._days.copy()
  154. def _evaluate(self, context: dict) -> bool:
  155. """Prüft ob aktueller Wochentag erlaubt ist."""
  156. now = context.get("current_time", datetime.now())
  157. if isinstance(now, datetime):
  158. weekday = now.weekday()
  159. else:
  160. weekday = datetime.now().weekday()
  161. return weekday in self._days
  162. def to_dict(self) -> dict:
  163. """Konvertiert zu Dictionary."""
  164. return {
  165. **self._base_dict(),
  166. "days": list(self._days),
  167. }
  168. @classmethod
  169. def from_dict(cls, data: dict) -> "DayOfWeekCondition":
  170. """Erstellt Condition aus Dictionary."""
  171. return cls(
  172. days=data["days"],
  173. name=data.get("name"),
  174. invert=data.get("invert", False),
  175. )
  176. @classmethod
  177. def display_name(cls) -> str:
  178. """Anzeigename fuer die TUI."""
  179. return "Wochentag"
  180. @classmethod
  181. def form_fields(cls) -> list[FormField]:
  182. """Gibt die Formular-Felder fuer die TUI zurueck."""
  183. return [
  184. FormField(
  185. name="days", label="Wochentage",
  186. field_type=FieldType.WEEKDAYS, required=True,
  187. help_text="Erlaubte Wochentage",
  188. ),
  189. FormField(
  190. name="invert", label="Invertieren",
  191. field_type=FieldType.CHECKBOX, default=False,
  192. ),
  193. ]
  194. @classmethod
  195. def validate_form(cls, data: dict) -> list[FormValidationError]:
  196. """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
  197. errors: list[FormValidationError] = []
  198. days = data.get("days", [])
  199. if not days:
  200. errors.append(FormValidationError("days", "Mindestens ein Wochentag muss ausgewaehlt sein"))
  201. return errors
  202. @classmethod
  203. def weekdays(cls, name: str | None = None) -> "DayOfWeekCondition":
  204. """Erstellt Condition für Werktage (Mo-Fr)."""
  205. return cls([0, 1, 2, 3, 4], name=name or "Weekdays")
  206. @classmethod
  207. def weekends(cls, name: str | None = None) -> "DayOfWeekCondition":
  208. """Erstellt Condition für Wochenenden (Sa-So)."""
  209. return cls([5, 6], name=name or "Weekends")
  210. class DateRangeCondition(Condition):
  211. """
  212. Prüft ob aktuelles Datum in einem Bereich liegt.
  213. Beispiel:
  214. - DateRangeCondition(date(2024, 12, 1), date(2024, 12, 31)) - Dezember 2024
  215. """
  216. def __init__(
  217. self,
  218. start_date: date | None = None,
  219. end_date: date | None = None,
  220. name: str | None = None,
  221. invert: bool = False,
  222. ):
  223. """
  224. Initialisiert die Condition.
  225. Args:
  226. start_date: Startdatum (None = unbegrenzt)
  227. end_date: Enddatum (None = unbegrenzt)
  228. name: Condition-Name
  229. invert: Ergebnis invertieren
  230. """
  231. super().__init__(name, invert)
  232. self._start_date = start_date
  233. self._end_date = end_date
  234. @property
  235. def start_date(self) -> date | None:
  236. """Startdatum."""
  237. return self._start_date
  238. @property
  239. def end_date(self) -> date | None:
  240. """Enddatum."""
  241. return self._end_date
  242. def _evaluate(self, context: dict) -> bool:
  243. """Prüft ob aktuelles Datum im Bereich liegt."""
  244. now = context.get("current_time", datetime.now())
  245. if isinstance(now, datetime):
  246. current_date = now.date()
  247. elif isinstance(now, date):
  248. current_date = now
  249. else:
  250. current_date = datetime.now().date()
  251. if self._start_date and current_date < self._start_date:
  252. return False
  253. if self._end_date and current_date > self._end_date:
  254. return False
  255. return True
  256. def to_dict(self) -> dict:
  257. """Konvertiert zu Dictionary."""
  258. return {
  259. **self._base_dict(),
  260. "start_date": self._start_date.isoformat() if self._start_date else None,
  261. "end_date": self._end_date.isoformat() if self._end_date else None,
  262. }
  263. @classmethod
  264. def from_dict(cls, data: dict) -> "DateRangeCondition":
  265. """Erstellt Condition aus Dictionary."""
  266. start = None
  267. end = None
  268. if data.get("start_date"):
  269. start = date.fromisoformat(data["start_date"])
  270. if data.get("end_date"):
  271. end = date.fromisoformat(data["end_date"])
  272. return cls(
  273. start_date=start,
  274. end_date=end,
  275. name=data.get("name"),
  276. invert=data.get("invert", False),
  277. )
  278. @classmethod
  279. def display_name(cls) -> str:
  280. """Anzeigename fuer die TUI."""
  281. return "Datumsbereich"
  282. @classmethod
  283. def form_fields(cls) -> list[FormField]:
  284. """Gibt die Formular-Felder fuer die TUI zurueck."""
  285. return [
  286. FormField(
  287. name="start_date", label="Startdatum",
  288. field_type=FieldType.DATETIME,
  289. placeholder="2024-01-01", help_text="Format: YYYY-MM-DD",
  290. ),
  291. FormField(
  292. name="end_date", label="Enddatum",
  293. field_type=FieldType.DATETIME,
  294. placeholder="2024-12-31", help_text="Format: YYYY-MM-DD",
  295. ),
  296. FormField(
  297. name="invert", label="Invertieren",
  298. field_type=FieldType.CHECKBOX, default=False,
  299. ),
  300. ]
  301. @classmethod
  302. def validate_form(cls, data: dict) -> list[FormValidationError]:
  303. """Validiert Formular-Daten. Gibt Liste von Fehlern zurueck."""
  304. errors: list[FormValidationError] = []
  305. for fn in ("start_date", "end_date"):
  306. val = data.get(fn, "")
  307. if val:
  308. try:
  309. date.fromisoformat(val)
  310. except ValueError:
  311. label = "Startdatum" if fn == "start_date" else "Enddatum"
  312. errors.append(FormValidationError(fn, f"Ungueltiges {label} (YYYY-MM-DD erwartet)"))
  313. return errors