From e16a41bc9ddd52e1dafa30081c99482389ef4863 Mon Sep 17 00:00:00 2001 From: Piotr Kaznowski Date: Fri, 29 Jan 2021 22:29:49 +0100 Subject: [PATCH] #5 adds pa schedule list command. by: Piotr --- cli/schedule.py | 38 ++++++++++++++++++++-- tests/test_cli_schedule.py | 64 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 5 deletions(-) diff --git a/cli/schedule.py b/cli/schedule.py index 173d4e2..a9a1413 100644 --- a/cli/schedule.py +++ b/cli/schedule.py @@ -3,7 +3,7 @@ import typer from tabulate import tabulate -from pythonanywhere.scripts_commons import get_logger, get_task_from_id +from pythonanywhere.scripts_commons import get_logger, get_task_from_id, tabulate_formats from pythonanywhere.snakesay import snakesay from pythonanywhere.task import Task, TaskList @@ -194,9 +194,41 @@ def get( logger.info(tabulate(table, tablefmt="simple")) +def tablefmt_callback(value: str): + if value not in tabulate_formats: + raise typer.BadParameter(f"Table format has to be one of: {', '.join(tabulate_formats)}") + return value + + @app.command("list") -def list_(): - raise NotImplementedError +def list_( + tablefmt: str = typer.Option( + "simple", "-f", "--format", help="Table format", callback=tablefmt_callback + ) +): + """Get list of user's scheduled tasks as a table with columns: + id, interval, at (hour:minute/minute past), status (enabled/disabled), command. + + Note: + This script provides an overview of all tasks. Once a task id is + known and some specific data is required it's more convenient to get + it using `pa schedule get` command instead of parsing the table. + """ + + logger = get_logger(set_info=True) + + headers = "id", "interval", "at", "status", "command" + attrs = "task_id", "interval", "printable_time", "enabled", "command" + + def stringify_values(task, attr): + value = getattr(task, attr) + if attr == "enabled": + value = "enabled" if value else "disabled" + return value + + table = [[stringify_values(task, attr) for attr in attrs] for task in TaskList().tasks] + msg = tabulate(table, headers, tablefmt=tablefmt) if table else snakesay("No scheduled tasks") + logger.info(msg) @app.command() diff --git a/tests/test_cli_schedule.py b/tests/test_cli_schedule.py index 944446b..50c9c9c 100644 --- a/tests/test_cli_schedule.py +++ b/tests/test_cli_schedule.py @@ -5,17 +5,35 @@ from typer.testing import CliRunner from cli.schedule import app, delete_app +from pythonanywhere.scripts_commons import tabulate_formats runner = CliRunner() @pytest.fixture def task_list(mocker): + username = getpass.getuser() + specs1 = { + "can_enable": False, + "command": "echo foo", + "enabled": True, + "expiry": None, + "extend_url": f"/user/{username}/schedule/task/42/extend", + "hour": 16, + "task_id": 42, + "interval": "daily", + "logfile": "/user/{username}/files/var/log/tasklog-126708-daily-at-1600-echo_foo.log", + "minute": 0, + "printable_time": "16:00", + "url": f"/api/v0/user/{username}/schedule/42", + "user": username, + } + specs2 = {**specs1} + specs2.update({"task_id": 43, "enabled": False}) mock_task_list = mocker.patch("cli.schedule.TaskList") - mock_task_list.return_value.tasks = [Mock(task_id=1), Mock(task_id=2)] + mock_task_list.return_value.tasks = [Mock(**specs1), Mock(**specs2)] return mock_task_list - @pytest.fixture def mock_confirm(mocker): return mocker.patch("cli.schedule.typer.confirm") @@ -213,3 +231,45 @@ def test_logs_only_value_of_requested_task_spec(self, mocker, task_from_id): assert task_from_id.call_args == call(42) assert mock_logger.call_args == call(set_info=True) assert mock_logger.return_value.info.call_args == call("10:23") +@pytest.mark.clischedulelist +class TestList: + def test_logs_table_with_correct_headers_and_values(self, mocker, task_list): + mock_logger = mocker.patch("cli.schedule.get_logger") + mock_tabulate = mocker.patch("cli.schedule.tabulate") + + result = runner.invoke(app, ["list", "--format", "orgtbl"]) + + headers = "id", "interval", "at", "status", "command" + attrs = "task_id", "interval", "printable_time", "enabled", "command" + table = [[getattr(task, attr) for attr in attrs] for task in task_list.return_value.tasks] + table = [ + ["enabled" if spec == True else "disabled" if spec == False else spec for spec in row] + for row in table + ] + assert mock_logger.call_args == call(set_info=True) + assert task_list.call_count == 1 + assert mock_tabulate.call_args == call(table, headers, tablefmt="orgtbl") + assert mock_logger.return_value.info.call_args == call(mock_tabulate.return_value) + + def test_snakesays_when_no_scheduled_tasks(self, mocker): + mock_logger = mocker.patch("cli.schedule.get_logger").return_value + mock_tabulate = mocker.patch("cli.schedule.tabulate") + mock_snakesay = mocker.patch("cli.schedule.snakesay") + mock_tasks = mocker.patch("cli.schedule.TaskList") + mock_tasks.return_value.tasks = [] + + runner.invoke(app, ["list"]) + + assert mock_tabulate.call_count == 0 + assert mock_snakesay.call_args == call("No scheduled tasks") + assert mock_logger.info.call_args == call(mock_snakesay.return_value) + + def test_warns_when_wrong_format_provided(self, mocker, task_list): + mock_tabulate = mocker.patch("cli.schedule.tabulate") + wrong_format = "excel" + + result = runner.invoke(app, ["list", "--format", "excel"]) + + assert mock_tabulate.call_count == 0 + assert wrong_format not in tabulate_formats + assert "Table format has to be one of" in result.stdout