Skip to content

Commit

Permalink
cli: add include-last-command flag to list and status commands
Browse files Browse the repository at this point in the history
Add `--include-last-command` flag to the `list` and `status` commands
that, when set, will display info about the command currently being
executed by the workflow (or the last submitted command). In case there
is no info about the command, the step name will be displayed, if
possible.

Closes reanahub/reana-workflow-controller#486.
  • Loading branch information
giuseppe-steduto committed Nov 24, 2023
1 parent 69acb62 commit 8e9c5a6
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changes
Version 0.9.2 (UNRELEASED)
--------------------------

- Changes ``status``, ``list`` commands to add the ``include-last-command`` flag to display the last command executed (or in execution) by the workflow.
- Fixes ``create_workflow_from_json`` API command to always send the workflow specification to the server.
- Fixes ``list`` command to be case-insensitive when using the ``--sort`` flag to sort the workflow runs by a specific column name.

Expand Down
10 changes: 8 additions & 2 deletions reana_client/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def get_workflows(
search=None,
include_progress=None,
include_workspace_size=None,
include_last_command=None,
workflow=None,
):
"""List all existing workflows.
Expand All @@ -130,6 +131,7 @@ def get_workflows(
:param search: search workflows by name.
:param include_progress: include progress information in the response.
:param include_workspace_size: include workspace size information in the response.
:param include_last_command: include info about the command currently executing.
:param workflow: name or id of the workflow.
:return: a list of dictionaries with the information about the workflows.
Expand All @@ -148,6 +150,7 @@ def get_workflows(
search=search,
include_progress=include_progress,
include_workspace_size=include_workspace_size,
include_last_command=include_last_command,
workflow_id_or_name=workflow,
).result()
if http_response.status_code == 200:
Expand All @@ -170,19 +173,22 @@ def get_workflows(
raise e


def get_workflow_status(workflow, access_token):
def get_workflow_status(workflow, access_token, include_last_command=False):
"""Get status of previously created workflow.
:param workflow: name or id of the workflow.
:param access_token: access token of the current user.
:param include_last_command: show the last command executed/ing in the workflow.
:return: a dictionary with the information about the workflow status.
The dictionary has the following keys: ``id``, ``logs``, ``name``,
``progress``, ``status``, ``user``.
"""
try:
response, http_response = current_rs_api_client.api.get_workflow_status(
workflow_id_or_name=workflow, access_token=access_token
workflow_id_or_name=workflow,
access_token=access_token,
include_last_command=include_last_command,
).result()
if http_response.status_code == 200:
return response
Expand Down
18 changes: 18 additions & 0 deletions reana_client/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import functools
import json
import os
import re
import shlex
import sys
from typing import Callable, NoReturn, Optional, List, Tuple, Union
Expand Down Expand Up @@ -277,6 +278,23 @@ def get_formatted_progress(progress):
return "{0}/{1}".format(finished_jobs, total_jobs)


def get_formatted_workflow_command(progress):
"""Return lastly executed command if possible, otherwise try to return the step name."""
current_command = progress.get("current_command")
if current_command:
# Change multiline commands to a single line, replacing any sequence of consecutive newlines with a semicolon.
current_command = re.sub(r"\n+", "; ", current_command)
if current_command.startswith('bash -c "cd '):
current_command = current_command[current_command.index(";") + 2 : -2]
return current_command
else:
if "current_step_name" in progress and progress.get("current_step_name"):
current_step_name = progress.get("current_step_name")
return current_step_name
else:
return "-"


def key_value_to_dict(ctx, param, value):
"""Convert tuple params to dictionary. e.g `(foo=bar)` to `{'foo': 'bar'}`.
Expand Down
81 changes: 52 additions & 29 deletions reana_client/cli/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
display_formatted_output,
format_session_uri,
get_formatted_progress,
get_formatted_workflow_command,
human_readable_or_raw_option,
key_value_to_dict,
parse_filter_parameters,
Expand Down Expand Up @@ -144,6 +145,13 @@ def workflow_execution_group(ctx):
default=None,
help="Include size information of the workspace.",
)
@click.option(
"--include-last-command",
"include_last_command",
is_flag=True,
default=None,
help="Include the information about the last command executed (or currently in execution) by the workflow.",
)
@click.option(
"--show-deleted-runs",
"show_deleted_runs",
Expand Down Expand Up @@ -172,6 +180,7 @@ def workflows_list( # noqa: C901
include_duration: bool,
include_progress,
include_workspace_size,
include_last_command,
show_deleted_runs: bool,
): # noqa: D301
"""List all workflows and sessions.
Expand Down Expand Up @@ -217,11 +226,13 @@ def workflows_list( # noqa: C901
search=search_filter,
include_progress=include_progress,
include_workspace_size=include_workspace_size,
include_last_command=include_last_command,
workflow=workflow,
)
verbose_headers = ["id", "user"]
workspace_size_header = ["size"]
progress_header = ["progress"]
command_header = ["last_command"]
duration_header = ["duration"]
headers = {
"batch": ["name", "run_number", "created", "started", "ended", "status"],
Expand All @@ -242,6 +253,8 @@ def workflows_list( # noqa: C901
headers[type] += progress_header
if verbose or include_duration:
headers[type] += duration_header
if verbose or include_last_command:
headers[type] += command_header

data = []
for workflow in response:
Expand All @@ -260,6 +273,8 @@ def workflows_list( # noqa: C901
value = None
if header in progress_header:
value = get_formatted_progress(workflow.get("progress"))
elif header in command_header:
value = get_formatted_workflow_command(workflow.get("progress"))
elif header in ["started", "ended"]:
_key = (
"run_started_at" if header == "started" else "run_finished_at"
Expand Down Expand Up @@ -692,12 +707,32 @@ def workflow_restart(
help="Include the duration of the workflow in seconds. In case the workflow is in "
"progress, its duration as of now will be shown.",
)
@click.option(
"--include-last-command",
"include_last_command",
is_flag=True,
default=False,
help="Include the information about the command that is currently being executed by the workflow.",
)
@click.option(
"-v",
"--verbose",
is_flag=True,
default=False,
help="Set status information verbosity.",
)
@add_access_token_options
@check_connection
@click.option("-v", "--verbose", count=True, help="Set status information verbosity.")
@click.pass_context
def workflow_status( # noqa: C901
ctx, workflow, _format, output_format, include_duration, access_token, verbose
ctx,
workflow,
_format,
output_format,
include_duration,
include_last_command,
verbose,
access_token,
): # noqa: D301
"""Get status of a workflow.
Expand Down Expand Up @@ -753,47 +788,35 @@ def add_data_from_response(row, data, headers):
data.append(parsed_response)
return data

def add_verbose_data_from_response(response, verbose_headers, headers, data):
def add_verbose_data_from_response(response, verbose_headers, data):
for k in verbose_headers:
if k == "command":
current_command = response["progress"]["current_command"]
if current_command:
if current_command.startswith('bash -c "cd '):
current_command = current_command[
current_command.index(";") + 2 : -2
]
data[-1] += [current_command]
else:
if "current_step_name" in response["progress"] and response[
"progress"
].get("current_step_name"):
current_step_name = response["progress"].get(
"current_step_name"
)
data[-1] += [current_step_name]
else:
headers.remove("command")
else:
data[-1] += [response.get(k)]
data[-1] += [response.get(k)]
return data

logging.debug("command: {}".format(ctx.command_path.replace(" ", ".")))
for p in ctx.params:
logging.debug("{param}: {value}".format(param=p, value=ctx.params[p]))
try:
workflow_response = get_workflow_status(workflow, access_token)
include_duration = verbose or include_duration
include_last_command = verbose or include_last_command
workflow_response = get_workflow_status(
workflow, access_token, include_last_command
)
headers = ["name", "run_number", "created", "status"]
verbose_headers = ["id", "user", "command"]
verbose_headers = ["id", "user"]
data = []
add_data_from_response(workflow_response, data, headers)
if verbose:
headers += verbose_headers
add_verbose_data_from_response(
workflow_response, verbose_headers, headers, data
)
if verbose or include_duration:
add_verbose_data_from_response(workflow_response, verbose_headers, data)
if include_duration:
headers += ["duration"]
data[-1] += [get_workflow_duration(workflow_response) or "-"]
if include_last_command:
headers += ["last_command"]
data[-1] += [
get_formatted_workflow_command(workflow_response.get("progress")) or "-"
]

display_formatted_output(data, headers, _format, output_format)

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"click>=7",
"pathspec==0.9.0",
"jsonpointer>=2.0",
"reana-commons[yadage,snakemake,cwl]>=0.9.4a1,<0.10.0",
"reana-commons[yadage,snakemake,cwl]>=0.9.4a3,<0.10.0",
"tablib>=0.12.1,<0.13",
"werkzeug>=0.14.1 ; python_version<'3.10'",
"werkzeug>=0.15.0 ; python_version>='3.10'",
Expand Down
76 changes: 76 additions & 0 deletions tests/test_cli_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,81 @@ def test_workflows_without_include_workspace_size():
assert "SIZE" not in result.output


def test_workflows_include_last_command():
"""Test workflows command with --include-last-command flag."""
response = {
"items": [
{
"status": "running",
"created": "2018-06-13T09:47:35.66097",
"user": "00000000-0000-0000-0000-000000000000",
"name": "mytest.1",
"id": "256b25f4-4cfb-4684-b7a8-73872ef455a1",
"size": {"human_readable": "15.97 MiB", "raw": 16741346},
"progress": {
"current_command": "some_command\n\nanother one\nlast one",
"run_started_at": "2021-05-10T12:55:04",
"run_finished_at": "2021-05-10T12:55:23",
},
}
]
}
status_code = 200
mock_http_response, mock_response = Mock(), Mock()
mock_http_response.status_code = status_code
mock_response = response
env = {"REANA_SERVER_URL": "localhost"}
reana_token = "000000"
runner = CliRunner(env=env)
with runner.isolation():
with patch(
"reana_client.api.client.current_rs_api_client",
make_mock_api_client("reana-server")(mock_response, mock_http_response),
):
result = runner.invoke(
cli, ["list", "--include-last-command", "-t", reana_token]
)
assert result.exit_code == 0
assert "LAST_COMMAND" in result.output
assert "some_command; another one; last one" in result.output


def test_workflows_without_include_last_command():
"""Test workflows command without --include-last-command flag."""
response = {
"items": [
{
"status": "running",
"created": "2018-06-13T09:47:35.66097",
"user": "00000000-0000-0000-0000-000000000000",
"name": "mytest.1",
"id": "256b25f4-4cfb-4684-b7a8-73872ef455a1",
"size": {"human_readable": "", "raw": -1},
"progress": {
"current_command": "some_command",
"run_started_at": "2021-05-10T12:55:04",
"run_finished_at": "2021-05-10T12:55:23",
},
}
]
}
status_code = 200
mock_http_response, mock_response = Mock(), Mock()
mock_http_response.status_code = status_code
mock_response = response
env = {"REANA_SERVER_URL": "localhost"}
reana_token = "000000"
runner = CliRunner(env=env)
with runner.isolation():
with patch(
"reana_client.api.client.current_rs_api_client",
make_mock_api_client("reana-server")(mock_response, mock_http_response),
):
result = runner.invoke(cli, ["list", "-t", reana_token])
assert result.exit_code == 0
assert "COMMAND" not in result.output


def test_workflows_format():
"""Test workflows command with --format."""
response = {
Expand Down Expand Up @@ -794,6 +869,7 @@ def test_get_workflow_status_ok():
assert isinstance(json_response, list)
assert len(json_response) == 1
assert json_response[0]["name"] in response["name"]
assert json_response[0]["last_command"] == "-"


@patch("reana_client.cli.workflow.workflow_create")
Expand Down
16 changes: 16 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from unittest.mock import patch
from datetime import datetime

import pytest
from reana_client.cli.utils import get_formatted_workflow_command
from reana_client.utils import get_workflow_duration


Expand Down Expand Up @@ -47,3 +49,17 @@ def test_duration_running_workflow():
*args, **kw
)
assert get_workflow_duration(workflow) == 60 + 11


@pytest.mark.parametrize(
"progress,expected_output",
[
({"current_command": 'bash -c "cd /some/path; some_command;"'}, "some_command"),
({"current_command": "some_command"}, "some_command"),
({"current_command": None, "current_step_name": "step_1"}, "step_1"),
({"current_command": None, "current_step_name": None}, "-"),
({}, "-"),
],
)
def test_get_formatted_workflow_command(progress, expected_output):
assert get_formatted_workflow_command(progress) == expected_output

0 comments on commit 8e9c5a6

Please sign in to comment.