Skip to content

Commit 4c44f51

Browse files
committed
Implement recurrence in the CLI/REPL
Signed-off-by: Mathias L. Baumann <[email protected]>
1 parent f646314 commit 4c44f51

File tree

3 files changed

+279
-15
lines changed

3 files changed

+279
-15
lines changed

src/frequenz/client/dispatch/__main__.py

Lines changed: 129 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,18 @@
1717
from prompt_toolkit.patch_stdout import patch_stdout
1818
from prompt_toolkit.shortcuts import CompleteStyle
1919

20+
from frequenz.client.dispatch.types import (
21+
EndCriteria,
22+
Frequency,
23+
RecurrenceRule,
24+
Weekday,
25+
)
26+
2027
from ._cli_types import (
2128
FuzzyDateTime,
2229
FuzzyIntRange,
2330
FuzzyTimeDelta,
24-
JsonParamType,
31+
JsonDictParamType,
2532
SelectorParamType,
2633
)
2734
from ._client import Client
@@ -98,6 +105,57 @@ async def list_(ctx: click.Context, /, **filters: Any) -> None:
98105
click.echo(f"{num_dispatches} dispatches total.")
99106

100107

108+
def parse_recurrence(kwargs: dict[str, Any]) -> RecurrenceRule | None:
109+
"""Parse recurrence rule from kwargs."""
110+
interval = kwargs.pop("interval", 0)
111+
by_minute = list(kwargs.pop("by_minute", []))
112+
by_hour = list(kwargs.pop("by_hour", []))
113+
by_weekday = [Weekday[weekday.upper()] for weekday in kwargs.pop("by_weekday", [])]
114+
by_monthday = list(kwargs.pop("by_monthday", []))
115+
116+
if not kwargs.get("frequency"):
117+
return None
118+
119+
return RecurrenceRule(
120+
frequency=Frequency[kwargs.pop("frequency")],
121+
interval=interval,
122+
end_criteria=(
123+
EndCriteria(
124+
count=kwargs.pop("count", None),
125+
until=kwargs.pop("until", None),
126+
)
127+
if kwargs.get("count") or kwargs.get("until")
128+
else None
129+
),
130+
byminutes=by_minute,
131+
byhours=by_hour,
132+
byweekdays=by_weekday,
133+
bymonthdays=by_monthday,
134+
)
135+
136+
137+
def validate_reccurance(ctx: click.Context, param: click.Parameter, value: Any) -> Any:
138+
"""Validate recurrence rule."""
139+
if param.name == "frequency":
140+
return value
141+
142+
count_param = param.name == "count" and value
143+
until_param = param.name == "until" and value
144+
145+
if (
146+
count_param
147+
and ctx.params.get("until") is not None
148+
or until_param
149+
and ctx.params.get("count") is not None
150+
):
151+
raise click.BadArgumentUsage("Only count or until can be set, not both.")
152+
153+
if value and ctx.params.get("frequency") is None:
154+
raise click.BadArgumentUsage(f"Frequency must be set to use {param.name}.")
155+
156+
return value
157+
158+
101159
@cli.command()
102160
@click.argument("microgrid-id", required=True, type=int)
103161
@click.argument(
@@ -111,10 +169,73 @@ async def list_(ctx: click.Context, /, **filters: Any) -> None:
111169
@click.option("--active", "-a", type=bool, default=True)
112170
@click.option("--dry-run", "-d", type=bool, default=False)
113171
@click.option(
114-
"--payload", "-p", type=JsonParamType(), help="JSON payload for the dispatch"
172+
"--payload", "-p", type=JsonDictParamType(), help="JSON payload for the dispatch"
115173
)
116-
@click.option("--recurrence", type=str, help="Recurrence rule (see documentation)")
117174
@click.pass_context
175+
@click.option(
176+
"--frequency",
177+
"-f",
178+
type=click.Choice(
179+
[
180+
frequency.name
181+
for frequency in Frequency
182+
if frequency != Frequency.UNSPECIFIED
183+
],
184+
case_sensitive=False,
185+
),
186+
help="Frequency of the dispatch",
187+
callback=validate_reccurance,
188+
is_eager=True,
189+
)
190+
@click.option(
191+
"--interval",
192+
type=int,
193+
help="Interval of the dispatch, based on frequency",
194+
default=0,
195+
)
196+
@click.option(
197+
"--count",
198+
type=int,
199+
help="Number of occurrences of the dispatch",
200+
callback=validate_reccurance,
201+
)
202+
@click.option(
203+
"--until",
204+
type=FuzzyDateTime(),
205+
help="End time of the dispatch",
206+
callback=validate_reccurance,
207+
)
208+
@click.option(
209+
"--by-minute",
210+
type=int,
211+
help="Minute of the hour for the dispatch",
212+
multiple=True,
213+
callback=validate_reccurance,
214+
)
215+
@click.option(
216+
"--by-hour",
217+
type=int,
218+
help="Hour of the day for the dispatch",
219+
multiple=True,
220+
callback=validate_reccurance,
221+
)
222+
@click.option(
223+
"--by-weekday",
224+
type=click.Choice(
225+
[weekday.name for weekday in Weekday if weekday != Weekday.UNSPECIFIED],
226+
case_sensitive=False,
227+
),
228+
help="Day of the week for the dispatch",
229+
multiple=True,
230+
callback=validate_reccurance,
231+
)
232+
@click.option(
233+
"--by-monthday",
234+
type=int,
235+
help="Day of the month for the dispatch",
236+
multiple=True,
237+
callback=validate_reccurance,
238+
)
118239
async def create(
119240
ctx: click.Context,
120241
/,
@@ -128,8 +249,12 @@ async def create(
128249
SELECTOR is either one of the following: BATTERY, GRID, METER, INVERTER,
129250
EV_CHARGER, CHP or a list of component IDs separated by commas, e.g. "1,2,3".
130251
"""
252+
# Remove keys with `None` value
253+
kwargs = {k: v for k, v in kwargs.items() if v is not None}
254+
131255
dispatch = await ctx.obj["client"].create(
132256
_type=kwargs.pop("type"),
257+
recurrence=parse_recurrence(kwargs),
133258
**kwargs,
134259
)
135260
click.echo(pformat(dispatch, compact=True))
@@ -166,7 +291,7 @@ async def update(
166291
@click.argument("dispatch_ids", type=int, nargs=-1) # Allow multiple IDs
167292
@click.pass_context
168293
async def get(ctx: click.Context, dispatch_ids: List[int]) -> None:
169-
"""Get multiple dispatches."""
294+
"""Get one or multiple dispatches."""
170295
num_failed = 0
171296

172297
for dispatch_id in dispatch_ids:

src/frequenz/client/dispatch/_cli_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def convert(
170170
)
171171

172172

173-
class JsonParamType(click.ParamType):
173+
class JsonDictParamType(click.ParamType):
174174
"""Click parameter type for JSON strings."""
175175

176176
name = "json"

0 commit comments

Comments
 (0)