Skip to content

Commit 804a0f2

Browse files
authored
Implement cli recurrence update (#51)
- **Make cli.update test more flexible** - **Client Bugfix: Actually update the payload** - **Add CLI support for updating the recurrence**
2 parents 332ead7 + 6bcf6ec commit 804a0f2

File tree

4 files changed

+172
-96
lines changed

4 files changed

+172
-96
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@
1414

1515
## Bug Fixes
1616

17-
<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
17+
* Fixed that client.update() was ignoring updates to "payload".

src/frequenz/client/dispatch/__main__.py

Lines changed: 92 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,73 @@ def validate_reccurance(ctx: click.Context, param: click.Parameter, value: Any)
156156
return value
157157

158158

159+
recurrence_options: list[click.Parameter] = [
160+
click.Option(
161+
["--frequency", "-f"],
162+
type=click.Choice(
163+
[
164+
frequency.name
165+
for frequency in Frequency
166+
if frequency != Frequency.UNSPECIFIED
167+
],
168+
case_sensitive=False,
169+
),
170+
help="Frequency of the dispatch",
171+
callback=validate_reccurance,
172+
is_eager=True,
173+
),
174+
click.Option(
175+
["--interval"],
176+
type=int,
177+
help="Interval of the dispatch, based on frequency",
178+
default=0,
179+
),
180+
click.Option(
181+
["--count"],
182+
type=int,
183+
help="Number of occurrences of the dispatch",
184+
callback=validate_reccurance,
185+
),
186+
click.Option(
187+
["--until"],
188+
type=FuzzyDateTime(),
189+
help="End time of the dispatch",
190+
callback=validate_reccurance,
191+
),
192+
click.Option(
193+
["--by-minute"],
194+
type=int,
195+
help="Minute of the hour for the dispatch",
196+
multiple=True,
197+
callback=validate_reccurance,
198+
),
199+
click.Option(
200+
["--by-hour"],
201+
type=int,
202+
help="Hour of the day for the dispatch",
203+
multiple=True,
204+
callback=validate_reccurance,
205+
),
206+
click.Option(
207+
["--by-weekday"],
208+
type=click.Choice(
209+
[weekday.name for weekday in Weekday if weekday != Weekday.UNSPECIFIED],
210+
case_sensitive=False,
211+
),
212+
help="Day of the week for the dispatch",
213+
multiple=True,
214+
callback=validate_reccurance,
215+
),
216+
click.Option(
217+
["--by-monthday"],
218+
type=int,
219+
help="Day of the month for the dispatch",
220+
multiple=True,
221+
callback=validate_reccurance,
222+
),
223+
]
224+
225+
159226
@cli.command()
160227
@click.argument("microgrid-id", required=True, type=int)
161228
@click.argument(
@@ -172,70 +239,6 @@ def validate_reccurance(ctx: click.Context, param: click.Parameter, value: Any)
172239
"--payload", "-p", type=JsonDictParamType(), help="JSON payload for the dispatch"
173240
)
174241
@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-
)
239242
async def create(
240243
ctx: click.Context,
241244
/,
@@ -269,20 +272,36 @@ async def create(
269272
@click.option("--duration", type=FuzzyTimeDelta())
270273
@click.option("--selector", type=SelectorParamType())
271274
@click.option("--active", type=bool)
275+
@click.option(
276+
"--payload", "-p", type=JsonDictParamType(), help="JSON payload for the dispatch"
277+
)
272278
@click.pass_context
273279
async def update(
274280
ctx: click.Context, dispatch_id: int, **new_fields: dict[str, Any]
275281
) -> None:
276282
"""Update a dispatch."""
277-
# Remove keys with `None` value from new_fields
278-
new_fields = {k: v for k, v in new_fields.items() if v is not None}
283+
284+
def skip_field(value: Any) -> bool:
285+
return value is None or value == [] or value == ()
286+
287+
# Every field is initialized with `None`, repeatable ones with `()` and `[]`.
288+
# We want to filter these out to not send them to the server.
289+
new_fields = {k: v for k, v in new_fields.items() if not skip_field(v)}
290+
recurrence = parse_recurrence(new_fields)
291+
292+
# Convert recurrence fields to nested fields as expected by update()
293+
for key in recurrence.__dict__ if recurrence else []:
294+
val = getattr(recurrence, key)
295+
if val is not None and val != []:
296+
new_fields[f"recurrence.{key}"] = val
279297

280298
if len(new_fields) == 0:
281299
raise click.BadArgumentUsage("At least one field must be given to update.")
282300

283301
try:
284302
await ctx.obj["client"].update(dispatch_id=dispatch_id, new_fields=new_fields)
285-
click.echo("Dispatch updated.")
303+
click.echo("Dispatch updated:")
304+
click.echo(pformat(await ctx.obj["client"].get(dispatch_id), compact=True))
286305
except grpc.RpcError as e:
287306
raise click.ClickException(f"Update failed: {e}")
288307

@@ -378,6 +397,12 @@ async def display_help() -> None:
378397
click.echo(e)
379398

380399

400+
# Add recurrence options to the create command
401+
create.params += recurrence_options # pylint: disable=no-member
402+
# Add recurrence options to the update command
403+
update.params += recurrence_options # pylint: disable=no-member
404+
405+
381406
def main() -> None:
382407
"""Entrypoint for the CLI."""
383408
if len(sys.argv) > 1:

src/frequenz/client/dispatch/_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ async def update(
211211
msg.update.selector.CopyFrom(component_selector_to_protobuf(val))
212212
case "is_active":
213213
msg.update.is_active = val
214+
case "payload":
215+
msg.update.payload.update(val)
214216
case "active":
215217
msg.update.is_active = val
216218
key = "is_active"

tests/test_dispatch_cli.py

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ async def test_create_command( # pylint: disable=too-many-arguments,too-many-lo
356356

357357
@pytest.mark.asyncio
358358
@pytest.mark.parametrize(
359-
"dispatches, field, value, update_field, update_value, expected_return_code, expected_output",
359+
"dispatches, args, fields, expected_return_code, expected_output",
360360
[
361361
(
362362
[
@@ -375,12 +375,13 @@ async def test_create_command( # pylint: disable=too-many-arguments,too-many-lo
375375
update_time=datetime(2023, 1, 1, 0, 0, 0),
376376
)
377377
],
378-
"--duration",
379-
"7200",
380-
"duration",
381-
timedelta(seconds=7200),
378+
[
379+
"--duration",
380+
"7200",
381+
],
382+
{"duration": timedelta(seconds=7200)},
382383
0,
383-
"Dispatch updated.",
384+
"duration=datetime.timedelta(seconds=7200)",
384385
),
385386
(
386387
[
@@ -399,12 +400,15 @@ async def test_create_command( # pylint: disable=too-many-arguments,too-many-lo
399400
update_time=datetime(2023, 1, 1, 0, 0, 0),
400401
)
401402
],
402-
"--active",
403-
"False",
404-
"active",
405-
False,
403+
[
404+
"--active",
405+
"False",
406+
],
407+
{
408+
"active": False,
409+
},
406410
0,
407-
"Dispatch updated.",
411+
"active=False",
408412
),
409413
(
410414
[
@@ -423,19 +427,64 @@ async def test_create_command( # pylint: disable=too-many-arguments,too-many-lo
423427
update_time=datetime(2023, 1, 1, 0, 0, 0),
424428
)
425429
],
426-
"--selector",
427-
"400, 401",
428-
"selector",
429-
[400, 401],
430+
[
431+
"--selector",
432+
"400, 401",
433+
"--frequency",
434+
"daily",
435+
"--interval",
436+
"5",
437+
"--count",
438+
"10",
439+
"--by-minute",
440+
"0",
441+
"--by-minute",
442+
"3",
443+
"--by-minute",
444+
"5",
445+
"--by-minute",
446+
"30",
447+
"--payload",
448+
'{"key": "value"}',
449+
],
450+
{
451+
"selector": [400, 401],
452+
"recurrence": RecurrenceRule(
453+
frequency=Frequency.DAILY,
454+
interval=5,
455+
end_criteria=EndCriteria(
456+
count=10,
457+
until=None,
458+
),
459+
byminutes=[0, 3, 5, 30],
460+
byhours=[],
461+
byweekdays=[],
462+
bymonthdays=[],
463+
),
464+
"payload": {"key": "value"},
465+
},
430466
0,
431-
"Dispatch updated.",
467+
""" selector=[400, 401],
468+
active=True,
469+
dry_run=False,
470+
payload={'key': 'value'},
471+
recurrence=RecurrenceRule(frequency=<Frequency.DAILY: 3>,
472+
interval=5,
473+
end_criteria=EndCriteria(count=10,
474+
until=None),
475+
byminutes=[0, 3, 5, 30],
476+
byhours=[],
477+
byweekdays=[],
478+
bymonthdays=[],
479+
bymonths=[]),""",
432480
),
433481
(
434482
[],
435-
"--duration",
436-
"frankly my dear, I don't give a damn",
437-
"",
438-
None,
483+
[
484+
"--duration",
485+
"frankly my dear, I don't give a damn",
486+
],
487+
{},
439488
2,
440489
"Error: Invalid value for '--duration': Could not parse time expression",
441490
),
@@ -445,20 +494,20 @@ async def test_update_command( # pylint: disable=too-many-arguments
445494
runner: CliRunner,
446495
fake_client: FakeClient,
447496
dispatches: list[Dispatch],
448-
field: str,
449-
value: str,
450-
update_field: str,
451-
update_value: Any,
497+
args: list[str],
498+
fields: dict[str, Any],
452499
expected_return_code: int,
453500
expected_output: str,
454501
) -> None:
455502
"""Test the update command."""
456503
fake_client.dispatches = dispatches
457-
result = await runner.invoke(cli, ["update", "1", field, value])
458-
assert result.exit_code == expected_return_code
504+
result = await runner.invoke(cli, ["update", "1", *args])
459505
assert expected_output in result.output
460-
if expected_return_code == 0:
461-
assert getattr(fake_client.dispatches[0], update_field) == update_value
506+
assert result.exit_code == expected_return_code
507+
if dispatches:
508+
assert len(fake_client.dispatches) == 1
509+
for key, value in fields.items():
510+
assert getattr(fake_client.dispatches[0], key) == value
462511

463512

464513
@pytest.mark.asyncio

0 commit comments

Comments
 (0)