Skip to content

Commit a6a2b1d

Browse files
authored
Move some high-level functions to this repo (frequenz-floss#103)
2 parents eae538a + 4678858 commit a6a2b1d

File tree

12 files changed

+639
-179
lines changed

12 files changed

+639
-179
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## New Features
44

55
* Update BaseApiClient to get the http2 keepalive feature.
6+
* Some Methods from the high-level API have been moved to this repo: The dispatch class now offers: `until`, `started`, `next_run` and `next_run_after`.
67

78
## Bug Fixes
89

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ dev-mypy = [
8080
"frequenz-client-dispatch[cli,dev-mkdocs,dev-noxfile,dev-pytest]",
8181
"grpc-stubs == 1.53.0.5",
8282
"types-protobuf == 5.28.3.20241030",
83+
"types-python-dateutil == 2.9.0.20241003",
8384
]
8485
dev-noxfile = ["nox == 2024.10.9", "frequenz-repo-config[lib] == 0.10.0"]
8586
dev-pylint = [
@@ -94,6 +95,7 @@ dev-pytest = [
9495
"pytest-mock == 3.14.0",
9596
"pytest-asyncio == 0.24.0",
9697
"async-solipsism == 0.7",
98+
"time-machine == 2.15.0",
9799
"hypothesis == 6.116.0",
98100
"frequenz-client-dispatch[cli]",
99101
]

src/frequenz/client/dispatch/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from prompt_toolkit.patch_stdout import patch_stdout
1717
from prompt_toolkit.shortcuts import CompleteStyle
1818

19-
from frequenz.client.dispatch.types import (
19+
from frequenz.client.dispatch.recurrence import (
2020
EndCriteria,
2121
Frequency,
2222
RecurrenceRule,

src/frequenz/client/dispatch/_client.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@
4040
from frequenz.client.base.streaming import GrpcStreamBroadcaster
4141

4242
from ._internal_types import DispatchCreateRequest
43+
from .recurrence import RecurrenceRule
4344
from .types import (
4445
ComponentSelector,
4546
Dispatch,
4647
DispatchEvent,
47-
RecurrenceRule,
48-
component_selector_to_protobuf,
48+
_component_selector_to_protobuf,
4949
)
5050

5151
# pylint: enable=no-name-in-module
@@ -166,7 +166,7 @@ def to_interval(
166166
# Setup parameters
167167
start_time_interval = to_interval(start_from, start_to)
168168
end_time_interval = to_interval(end_from, end_to)
169-
selectors = list(map(component_selector_to_protobuf, component_selectors))
169+
selectors = list(map(_component_selector_to_protobuf, component_selectors))
170170
filters = DispatchFilter(
171171
selectors=selectors,
172172
start_time_interval=start_time_interval,
@@ -354,7 +354,7 @@ async def update(
354354
else:
355355
msg.update.duration = round(val.total_seconds())
356356
case "selector":
357-
msg.update.selector.CopyFrom(component_selector_to_protobuf(val))
357+
msg.update.selector.CopyFrom(_component_selector_to_protobuf(val))
358358
case "is_active":
359359
msg.update.is_active = val
360360
case "payload":

src/frequenz/client/dispatch/_internal_types.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818

1919
from frequenz.client.base.conversion import to_datetime, to_timestamp
2020

21+
from .recurrence import RecurrenceRule
2122
from .types import (
2223
ComponentSelector,
23-
RecurrenceRule,
24-
component_selector_from_protobuf,
25-
component_selector_to_protobuf,
24+
_component_selector_from_protobuf,
25+
_component_selector_to_protobuf,
2626
)
2727

2828
# pylint: enable=no-name-in-module
@@ -97,7 +97,9 @@ def from_protobuf(
9797
to_datetime(pb_object.dispatch_data.start_time)
9898
),
9999
duration=duration,
100-
selector=component_selector_from_protobuf(pb_object.dispatch_data.selector),
100+
selector=_component_selector_from_protobuf(
101+
pb_object.dispatch_data.selector
102+
),
101103
active=pb_object.dispatch_data.is_active,
102104
dry_run=pb_object.dispatch_data.is_dry_run,
103105
payload=MessageToDict(pb_object.dispatch_data.payload),
@@ -121,7 +123,7 @@ def to_protobuf(self) -> PBDispatchCreateRequest:
121123
duration=(
122124
round(self.duration.total_seconds()) if self.duration else None
123125
),
124-
selector=component_selector_to_protobuf(self.selector),
126+
selector=_component_selector_to_protobuf(self.selector),
125127
is_active=self.active,
126128
is_dry_run=self.dry_run,
127129
payload=payload,
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Types for recurrence rules."""
5+
6+
from dataclasses import dataclass, field
7+
from datetime import datetime
8+
from enum import IntEnum
9+
10+
from dateutil import rrule
11+
12+
# pylint: disable=no-name-in-module
13+
from frequenz.api.dispatch.v1.dispatch_pb2 import RecurrenceRule as PBRecurrenceRule
14+
15+
from frequenz.client.base.conversion import to_datetime, to_timestamp
16+
17+
# pylint: enable=no-name-in-module
18+
19+
20+
class Weekday(IntEnum):
21+
"""Enum representing the day of the week."""
22+
23+
UNSPECIFIED = PBRecurrenceRule.WEEKDAY_UNSPECIFIED
24+
MONDAY = PBRecurrenceRule.WEEKDAY_MONDAY
25+
TUESDAY = PBRecurrenceRule.WEEKDAY_TUESDAY
26+
WEDNESDAY = PBRecurrenceRule.WEEKDAY_WEDNESDAY
27+
THURSDAY = PBRecurrenceRule.WEEKDAY_THURSDAY
28+
FRIDAY = PBRecurrenceRule.WEEKDAY_FRIDAY
29+
SATURDAY = PBRecurrenceRule.WEEKDAY_SATURDAY
30+
SUNDAY = PBRecurrenceRule.WEEKDAY_SUNDAY
31+
32+
33+
class Frequency(IntEnum):
34+
"""Enum representing the frequency of the recurrence."""
35+
36+
UNSPECIFIED = PBRecurrenceRule.FREQUENCY_UNSPECIFIED
37+
MINUTELY = PBRecurrenceRule.FREQUENCY_MINUTELY
38+
HOURLY = PBRecurrenceRule.FREQUENCY_HOURLY
39+
DAILY = PBRecurrenceRule.FREQUENCY_DAILY
40+
WEEKLY = PBRecurrenceRule.FREQUENCY_WEEKLY
41+
MONTHLY = PBRecurrenceRule.FREQUENCY_MONTHLY
42+
YEARLY = PBRecurrenceRule.FREQUENCY_YEARLY
43+
44+
45+
_RRULE_FREQ_MAP = {
46+
Frequency.MINUTELY: rrule.MINUTELY,
47+
Frequency.HOURLY: rrule.HOURLY,
48+
Frequency.DAILY: rrule.DAILY,
49+
Frequency.WEEKLY: rrule.WEEKLY,
50+
Frequency.MONTHLY: rrule.MONTHLY,
51+
}
52+
"""To map from our Frequency enum to the dateutil library enum."""
53+
54+
_RRULE_WEEKDAY_MAP = {
55+
Weekday.MONDAY: rrule.MO,
56+
Weekday.TUESDAY: rrule.TU,
57+
Weekday.WEDNESDAY: rrule.WE,
58+
Weekday.THURSDAY: rrule.TH,
59+
Weekday.FRIDAY: rrule.FR,
60+
Weekday.SATURDAY: rrule.SA,
61+
Weekday.SUNDAY: rrule.SU,
62+
}
63+
"""To map from our Weekday enum to the dateutil library enum."""
64+
65+
66+
@dataclass(kw_only=True)
67+
class EndCriteria:
68+
"""Controls when a recurring dispatch should end."""
69+
70+
count: int | None = None
71+
"""The number of times this dispatch should recur."""
72+
until: datetime | None = None
73+
"""The end time of this dispatch in UTC."""
74+
75+
@classmethod
76+
def from_protobuf(cls, pb_criteria: PBRecurrenceRule.EndCriteria) -> "EndCriteria":
77+
"""Convert a protobuf end criteria to an end criteria.
78+
79+
Args:
80+
pb_criteria: The protobuf end criteria to convert.
81+
82+
Returns:
83+
The converted end criteria.
84+
"""
85+
instance = cls()
86+
87+
match pb_criteria.WhichOneof("count_or_until"):
88+
case "count":
89+
instance.count = pb_criteria.count
90+
case "until":
91+
instance.until = to_datetime(pb_criteria.until)
92+
return instance
93+
94+
def to_protobuf(self) -> PBRecurrenceRule.EndCriteria:
95+
"""Convert an end criteria to a protobuf end criteria.
96+
97+
Returns:
98+
The converted protobuf end criteria.
99+
"""
100+
pb_criteria = PBRecurrenceRule.EndCriteria()
101+
102+
if self.count is not None:
103+
pb_criteria.count = self.count
104+
elif self.until is not None:
105+
pb_criteria.until.CopyFrom(to_timestamp(self.until))
106+
107+
return pb_criteria
108+
109+
110+
# pylint: disable=too-many-instance-attributes
111+
@dataclass(kw_only=True)
112+
class RecurrenceRule:
113+
"""Ruleset governing when and how a dispatch should re-occur.
114+
115+
Attributes follow the iCalendar specification (RFC5545) for recurrence rules.
116+
"""
117+
118+
frequency: Frequency = Frequency.UNSPECIFIED
119+
"""The frequency specifier of this recurring dispatch."""
120+
121+
interval: int = 0
122+
"""How often this dispatch should recur, based on the frequency."""
123+
124+
end_criteria: EndCriteria | None = None
125+
"""When this dispatch should end.
126+
127+
Can recur a fixed number of times or until a given timestamp."""
128+
129+
byminutes: list[int] = field(default_factory=list)
130+
"""On which minute(s) of the hour the event occurs."""
131+
132+
byhours: list[int] = field(default_factory=list)
133+
"""On which hour(s) of the day the event occurs."""
134+
135+
byweekdays: list[Weekday] = field(default_factory=list)
136+
"""On which day(s) of the week the event occurs."""
137+
138+
bymonthdays: list[int] = field(default_factory=list)
139+
"""On which day(s) of the month the event occurs."""
140+
141+
bymonths: list[int] = field(default_factory=list)
142+
"""On which month(s) of the year the event occurs."""
143+
144+
@classmethod
145+
def from_protobuf(cls, pb_rule: PBRecurrenceRule) -> "RecurrenceRule":
146+
"""Convert a protobuf recurrence rule to a recurrence rule.
147+
148+
Args:
149+
pb_rule: The protobuf recurrence rule to convert.
150+
151+
Returns:
152+
The converted recurrence rule.
153+
"""
154+
return RecurrenceRule(
155+
frequency=Frequency(pb_rule.freq),
156+
interval=pb_rule.interval,
157+
end_criteria=(
158+
EndCriteria.from_protobuf(pb_rule.end_criteria)
159+
if pb_rule.HasField("end_criteria")
160+
else None
161+
),
162+
byminutes=list(pb_rule.byminutes),
163+
byhours=list(pb_rule.byhours),
164+
byweekdays=[Weekday(day) for day in pb_rule.byweekdays],
165+
bymonthdays=list(pb_rule.bymonthdays),
166+
bymonths=list(pb_rule.bymonths),
167+
)
168+
169+
def to_protobuf(self) -> PBRecurrenceRule:
170+
"""Convert a recurrence rule to a protobuf recurrence rule.
171+
172+
Returns:
173+
The converted protobuf recurrence rule.
174+
"""
175+
pb_rule = PBRecurrenceRule()
176+
177+
pb_rule.freq = self.frequency.value
178+
pb_rule.interval = self.interval
179+
if self.end_criteria is not None:
180+
pb_rule.end_criteria.CopyFrom(self.end_criteria.to_protobuf())
181+
pb_rule.byminutes.extend(self.byminutes)
182+
pb_rule.byhours.extend(self.byhours)
183+
pb_rule.byweekdays.extend([day.value for day in self.byweekdays])
184+
pb_rule.bymonthdays.extend(self.bymonthdays)
185+
pb_rule.bymonths.extend(self.bymonths)
186+
187+
return pb_rule
188+
189+
def _as_rrule(self, start_time: datetime) -> rrule.rrule:
190+
"""Prepare the rrule object.
191+
192+
Args:
193+
start_time: The start time of the dispatch.
194+
195+
Returns:
196+
The rrule object.
197+
198+
Raises:
199+
ValueError: If the interval is 0.
200+
"""
201+
if self.interval == 0:
202+
raise ValueError("Interval must be greater than 0")
203+
204+
count, until = (None, None)
205+
if end := self.end_criteria:
206+
count = end.count
207+
until = end.until
208+
209+
rrule_obj = rrule.rrule(
210+
freq=_RRULE_FREQ_MAP[self.frequency],
211+
dtstart=start_time,
212+
count=count,
213+
until=until,
214+
byminute=self.byminutes or None,
215+
byhour=self.byhours or None,
216+
byweekday=[_RRULE_WEEKDAY_MAP[weekday] for weekday in self.byweekdays]
217+
or None,
218+
bymonthday=self.bymonthdays or None,
219+
bymonth=self.bymonths or None,
220+
interval=self.interval,
221+
)
222+
223+
return rrule_obj

src/frequenz/client/dispatch/test/generator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from frequenz.client.common.microgrid.components import ComponentCategory
1010

1111
from .._internal_types import rounded_start_time
12-
from ..types import Dispatch, EndCriteria, Frequency, RecurrenceRule, Weekday
12+
from ..recurrence import EndCriteria, Frequency, RecurrenceRule, Weekday
13+
from ..types import Dispatch
1314

1415

1516
class DispatchGenerator:

0 commit comments

Comments
 (0)