33
44"""Dispatch type with support for next_run calculation."""
55
6-
7- import logging
86from dataclasses import dataclass
97from datetime import datetime , timezone
10- from enum import Enum
11- from typing import Iterator , cast
8+ from typing import Iterator
129
13- from dateutil import rrule
14- from frequenz .client .dispatch .recurrence import Frequency , Weekday
1510from frequenz .client .dispatch .types import Dispatch as BaseDispatch
1611
17- _logger = logging .getLogger (__name__ )
18- """The logger for this module."""
19-
20- _RRULE_FREQ_MAP = {
21- Frequency .MINUTELY : rrule .MINUTELY ,
22- Frequency .HOURLY : rrule .HOURLY ,
23- Frequency .DAILY : rrule .DAILY ,
24- Frequency .WEEKLY : rrule .WEEKLY ,
25- Frequency .MONTHLY : rrule .MONTHLY ,
26- }
27- """To map from our Frequency enum to the dateutil library enum."""
28-
29- _RRULE_WEEKDAY_MAP = {
30- Weekday .MONDAY : rrule .MO ,
31- Weekday .TUESDAY : rrule .TU ,
32- Weekday .WEDNESDAY : rrule .WE ,
33- Weekday .THURSDAY : rrule .TH ,
34- Weekday .FRIDAY : rrule .FR ,
35- Weekday .SATURDAY : rrule .SA ,
36- Weekday .SUNDAY : rrule .SU ,
37- }
38- """To map from our Weekday enum to the dateutil library enum."""
39-
40-
41- class RunningState (Enum ):
42- """The running state of a dispatch."""
43-
44- RUNNING = "RUNNING"
45- """The dispatch is running."""
46-
47- STOPPED = "STOPPED"
48- """The dispatch is stopped."""
49-
50- DIFFERENT_TYPE = "DIFFERENT_TYPE"
51- """The dispatch is for a different type."""
52-
5312
5413@dataclass (frozen = True )
5514class Dispatch (BaseDispatch ):
@@ -87,6 +46,18 @@ def _set_deleted(self) -> None:
8746 """Mark the dispatch as deleted."""
8847 object .__setattr__ (self , "deleted" , True )
8948
49+ @property
50+ def started (self ) -> bool :
51+ """Check if the dispatch is started.
52+
53+ Returns:
54+ True if the dispatch is started, False otherwise.
55+ """
56+ if self .deleted :
57+ return False
58+
59+ return super ().started
60+
9061 @property
9162 def _running_status_notified (self ) -> bool :
9263 """Check that the latest running state change notification was sent.
@@ -100,52 +71,6 @@ def _set_running_status_notified(self) -> None:
10071 """Mark the latest running state change notification as sent."""
10172 object .__setattr__ (self , "running_state_change_synced" , self .update_time )
10273
103- def running (self , type_ : str ) -> RunningState :
104- """Check if the dispatch is currently supposed to be running.
105-
106- Args:
107- type_: The type of the dispatch that should be running.
108-
109- Returns:
110- RUNNING if the dispatch is running,
111- STOPPED if it is stopped,
112- DIFFERENT_TYPE if it is for a different type.
113- """
114- if self .type != type_ :
115- return RunningState .DIFFERENT_TYPE
116-
117- if not self .active or self .deleted :
118- return RunningState .STOPPED
119-
120- now = datetime .now (tz = timezone .utc )
121-
122- if now < self .start_time :
123- return RunningState .STOPPED
124- # A dispatch without duration is always running once it started
125- if self .duration is None :
126- return RunningState .RUNNING
127-
128- if until := self ._until (now ):
129- return RunningState .RUNNING if now < until else RunningState .STOPPED
130-
131- return RunningState .STOPPED
132-
133- @property
134- def until (self ) -> datetime | None :
135- """Time when the dispatch should end.
136-
137- Returns the time that a running dispatch should end.
138- If the dispatch is not running, None is returned.
139-
140- Returns:
141- The time when the dispatch should end or None if the dispatch is not running.
142- """
143- if not self .active or self .deleted :
144- return None
145-
146- now = datetime .now (tz = timezone .utc )
147- return self ._until (now )
148-
14974 @property
15075 # noqa is needed because of a bug in pydoclint that makes it think a `return` without a return
15176 # value needs documenting
@@ -170,103 +95,3 @@ def missed_runs(self) -> Iterator[datetime]: # noqa: DOC405
17095 while (next_run := self .next_run_after (from_time )) and next_run < now :
17196 yield next_run
17297 from_time = next_run
173-
174- @property
175- def next_run (self ) -> datetime | None :
176- """Calculate the next run of a dispatch.
177-
178- Returns:
179- The next run of the dispatch or None if the dispatch is finished.
180- """
181- return self .next_run_after (datetime .now (tz = timezone .utc ))
182-
183- def next_run_after (self , after : datetime ) -> datetime | None :
184- """Calculate the next run of a dispatch.
185-
186- Args:
187- after: The time to calculate the next run from.
188-
189- Returns:
190- The next run of the dispatch or None if the dispatch is finished.
191- """
192- if (
193- not self .recurrence .frequency
194- or self .recurrence .frequency == Frequency .UNSPECIFIED
195- or self .duration is None # Infinite duration
196- ):
197- if after > self .start_time :
198- return None
199- return self .start_time
200-
201- # Make sure no weekday is UNSPECIFIED
202- if Weekday .UNSPECIFIED in self .recurrence .byweekdays :
203- _logger .warning ("Dispatch %s has UNSPECIFIED weekday, ignoring..." , self .id )
204- return None
205-
206- # No type information for rrule, so we need to cast
207- return cast (datetime | None , self ._prepare_rrule ().after (after , inc = True ))
208-
209- def _prepare_rrule (self ) -> rrule .rrule :
210- """Prepare the rrule object.
211-
212- Returns:
213- The rrule object.
214-
215- Raises:
216- ValueError: If the interval is invalid.
217- """
218- count , until = (None , None )
219- if end := self .recurrence .end_criteria :
220- count = end .count
221- until = end .until
222-
223- if self .recurrence .interval is None or self .recurrence .interval < 1 :
224- raise ValueError ("Interval must be at least 1" )
225-
226- rrule_obj = rrule .rrule (
227- freq = _RRULE_FREQ_MAP [self .recurrence .frequency ],
228- dtstart = self .start_time ,
229- count = count ,
230- until = until ,
231- byminute = self .recurrence .byminutes or None ,
232- byhour = self .recurrence .byhours or None ,
233- byweekday = [
234- _RRULE_WEEKDAY_MAP [weekday ] for weekday in self .recurrence .byweekdays
235- ]
236- or None ,
237- bymonthday = self .recurrence .bymonthdays or None ,
238- bymonth = self .recurrence .bymonths or None ,
239- interval = self .recurrence .interval ,
240- )
241-
242- return rrule_obj
243-
244- def _until (self , now : datetime ) -> datetime | None :
245- """Calculate the time when the dispatch should end.
246-
247- If no previous run is found, None is returned.
248-
249- Args:
250- now: The current time.
251-
252- Returns:
253- The time when the dispatch should end or None if the dispatch is not running.
254-
255- Raises:
256- ValueError: If the dispatch has no duration.
257- """
258- if self .duration is None :
259- raise ValueError ("_until: Dispatch has no duration" )
260-
261- if (
262- not self .recurrence .frequency
263- or self .recurrence .frequency == Frequency .UNSPECIFIED
264- ):
265- return self .start_time + self .duration
266-
267- latest_past_start : datetime | None = self ._prepare_rrule ().before (now , inc = True )
268-
269- if not latest_past_start :
270- return None
271-
272- return latest_past_start + self .duration
0 commit comments