Skip to content

Commit db104cd

Browse files
committed
#5 adds pa schedule update command. by: Piotr
1 parent e16a41b commit db104cd

File tree

2 files changed

+177
-2
lines changed

2 files changed

+177
-2
lines changed

cli/schedule.py

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import logging
2+
import sys
3+
from datetime import datetime
14
from typing import List
25

36
import typer
@@ -232,5 +235,104 @@ def stringify_values(task, attr):
232235

233236

234237
@app.command()
235-
def update():
236-
raise NotImplementedError
238+
def update(
239+
task_id: int = typer.Argument(..., metavar="id"),
240+
command: str = typer.Option(
241+
None,
242+
"-c",
243+
"--command",
244+
help="Changes command to COMMAND (multiword commands should be quoted)"
245+
),
246+
hour: int = typer.Option(
247+
None,
248+
"-o",
249+
"--hour",
250+
min=0,
251+
max=23,
252+
help="Changes hour to HOUR (in 24h format)"
253+
),
254+
minute: int = typer.Option(
255+
None,
256+
"-m",
257+
"--minute",
258+
min=0,
259+
max=59,
260+
help="Changes minute to MINUTE"
261+
),
262+
disable: bool = typer.Option(False, "-d", "--disable", help="Disables task"),
263+
enable: bool = typer.Option(False, "-e", "--enable", help="Enables task"),
264+
toggle_enabled: bool = typer.Option(
265+
False, "-t", "--toggle-enabled", help="Toggles enable/disable state"
266+
),
267+
daily: bool = typer.Option(
268+
False,
269+
"-a",
270+
"--daily",
271+
help=(
272+
"Switches interval to daily "
273+
"(when --hour is not provided, sets it automatically to current hour)"
274+
)
275+
),
276+
hourly: bool = typer.Option(
277+
False,
278+
"-u",
279+
"--hourly",
280+
help="Switches interval to hourly (takes precedence over --hour, i.e. sets hour to None)"
281+
),
282+
quiet: bool = typer.Option(False, "-q", "--quiet", help="Turns off messages"),
283+
porcelain: bool = typer.Option(
284+
False, "-p", "--porcelain", help="Prints message in easy-to-parse format"
285+
),
286+
):
287+
"""Update a scheduled task.
288+
289+
Note that logfile name will change after updating the task but it won't be
290+
created until first execution of the task.
291+
To change interval from hourly to daily use --daily flag and provide --hour.
292+
When --daily flag is not accompanied with --hour, new hour for the task
293+
will be automatically set to current hour.
294+
When changing interval from daily to hourly --hour flag is ignored.
295+
296+
Example:
297+
Change command for a scheduled task 42:
298+
299+
pa schedule update 42 --command "echo new command"
300+
301+
Change interval of the task 42 from hourly to daily to be run at 10 am:
302+
303+
pa schedule update 42 --hour 10
304+
305+
Change interval of the task 42 from daily to hourly and set new minute:
306+
307+
pa schedule update 42 --minute 13 --hourly"""
308+
309+
kwargs = {k: v for k, v in locals().items() if k != "task_id"}
310+
logger = get_logger()
311+
312+
porcelain = kwargs.pop("porcelain")
313+
if not kwargs.pop("quiet"):
314+
logger.setLevel(logging.INFO)
315+
316+
if not any(kwargs.values()):
317+
msg = "Nothing to update!"
318+
logger.warning(msg if porcelain else snakesay(msg))
319+
sys.exit(1)
320+
321+
if kwargs.pop("hourly"):
322+
kwargs["interval"] = "hourly"
323+
if kwargs.pop("daily"):
324+
kwargs["hour"] = kwargs["hour"] if kwargs["hour"] else datetime.now().hour
325+
kwargs["interval"] = "daily"
326+
327+
task = get_task_from_id(task_id)
328+
329+
enable_opt = [k for k in ["toggle_enabled", "disable", "enable"] if kwargs.pop(k)]
330+
params = {k: v for k, v in kwargs.items() if v}
331+
if enable_opt:
332+
lookup = {"toggle_enabled": not task.enabled, "disable": False, "enable": True}
333+
params.update({"enabled": lookup[enable_opt[0]]})
334+
335+
try:
336+
task.update_schedule(params, porcelain=porcelain)
337+
except Exception as e:
338+
logger.warning(snakesay(str(e)))

tests/test_cli_schedule.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,76 @@ def test_warns_when_wrong_format_provided(self, mocker, task_list):
273273
assert mock_tabulate.call_count == 0
274274
assert wrong_format not in tabulate_formats
275275
assert "Table format has to be one of" in result.stdout
276+
277+
278+
@pytest.mark.clischeduleupdate
279+
class TestUpdate:
280+
def test_enables_task_and_sets_porcelain(self, mocker):
281+
mock_task_from_id = mocker.patch("cli.schedule.get_task_from_id")
282+
283+
runner.invoke(app, ["update", "42", "--enable", "--porcelain"])
284+
285+
assert mock_task_from_id.call_args == call(42)
286+
assert mock_task_from_id.return_value.method_calls == [
287+
call.update_schedule({"enabled": True}, porcelain=True)
288+
]
289+
290+
def test_turns_off_snakesay(self, mocker):
291+
mock_logger = mocker.patch("cli.schedule.get_logger")
292+
293+
runner.invoke(app, ["update", "42", "--quiet"])
294+
295+
assert mock_logger.return_value.setLevel.call_count == 0
296+
297+
def test_warns_when_task_update_schedule_raises(self, mocker):
298+
mock_logger = mocker.patch("cli.schedule.get_logger")
299+
mock_task_from_id = mocker.patch("cli.schedule.get_task_from_id")
300+
mock_task_from_id.return_value.update_schedule.side_effect = Exception("error")
301+
mock_snake = mocker.patch("cli.schedule.snakesay")
302+
303+
runner.invoke(app, ["update", "42", "--disable"])
304+
305+
assert mock_snake.call_args == call("error")
306+
assert mock_logger.return_value.warning.call_args == call(mock_snake.return_value)
307+
308+
def test_ensures_proper_daily_params(self, mocker):
309+
mock_task_from_id = mocker.patch("cli.schedule.get_task_from_id")
310+
311+
result = runner.invoke(app, ["update", "42", "--hourly"])
312+
313+
assert mock_task_from_id.return_value.update_schedule.call_args == call(
314+
{"interval": "hourly"}, porcelain=False
315+
)
316+
317+
def test_ensures_proper_hourly_params(self, mocker):
318+
mock_task_from_id = mocker.patch("cli.schedule.get_task_from_id")
319+
mock_datetime = mocker.patch("cli.schedule.datetime")
320+
321+
runner.invoke(app, ["update", "42", "--daily"])
322+
323+
assert mock_task_from_id.return_value.update_schedule.call_args == call(
324+
{"interval": "daily", "hour": mock_datetime.now.return_value.hour},
325+
porcelain=False
326+
)
327+
328+
def test_validates_minute(self):
329+
result = runner.invoke(app, ["update", "42", "--minute", "88"])
330+
assert "88 is not in the valid range of 0 to 59" in result.stdout
331+
332+
def test_validates_hour(self):
333+
result = runner.invoke(app, ["update", "42", "--daily", "--hour", "33"])
334+
assert "33 is not in the valid range of 0 to 23" in result.stdout
335+
336+
def test_complains_when_no_id_provided(self):
337+
result = runner.invoke(app, ["update"])
338+
assert "Missing argument 'id'" in result.stdout
339+
340+
def test_exits_early_when_nothing_to_update(self, mocker):
341+
mock_logger = mocker.patch("cli.schedule.get_logger").return_value
342+
mock_snakesay = mocker.patch("cli.schedule.snakesay")
343+
344+
result = runner.invoke(app, ["update", "42"])
345+
346+
assert mock_snakesay.call_args == call("Nothing to update!")
347+
assert mock_logger.warning.call_args == call(mock_snakesay.return_value)
348+
assert result.exit_code == 1

0 commit comments

Comments
 (0)