Repository: reanahub/reana-client

Issue:
cli: show workspace retention rules to the users
According to our plans in #617, the MVP version of the workspace retention rules feature for reana-client is completed. We have `create` and `validation` parts finished.

As noted by @mdonadoni, there is no way to see retention rules using CLI. You can only use the web page. It would be beneficial to allow the same for CLI. 

We can display retention rules alongside files in `ls` or `du` commands as described in #617, but it might be complicated. The simpler version can be a new command that lists retention rules for a specific workflow like this:

```
$ reana-client retention-rules-show
RULE    DAYS   APPLY_ON 
**/*    365    <date>
```

Whatever approach we choose is something to decide on, but it would be good to have some way of displaying retention rules in CLI.

Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index a911777..49ea1c7 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -7,6 +7,7 @@ Version 0.9.0 (UNRELEASED)
 - Adds support for Python 3.11.
 - Adds support for ``.reanaignore`` during file upload. Files that match ``.reanaignore`` will not be uploaded.
 - Adds support for ``.gitignore`` during file upload. Files that match ``.gitignore`` will not be uploaded.
+- Adds ``retention-rules-list`` command to list the retention rules of a workflow.
 - Changes ``delete`` CLI command to always delete workspace.
 - Changes ``delete_workflow`` API method to always delete workspace.
 - Changes ``list`` to hide deleted workflows by default.
diff --git a/docs/cmd_list.txt b/docs/cmd_list.txt
index c6e4143..6623ab7 100644
--- a/docs/cmd_list.txt
+++ b/docs/cmd_list.txt
@@ -42,6 +42,9 @@ Workspace file management commands:
   rm        Delete files from workspace.
   upload    Upload files and directories to workspace.
 
+Workspace file retention commands:
+  retention-rules-list  List the retention rules for a workflow.
+
 Secret management commands:
   secrets-add     Add secrets from literal string or from file.
   secrets-delete  Delete user secrets by name.
diff --git a/reana_client/api/client.py b/reana_client/api/client.py
index 3975688..f3d1d5f 100644
--- a/reana_client/api/client.py
+++ b/reana_client/api/client.py
@@ -1062,3 +1062,32 @@ def info(access_token):
         raise Exception(e.response.json()["message"])
     except Exception as e:
         raise e
+
+
+def get_workflow_retention_rules(workflow, access_token):
+    """Get the retention rules of a workflow."""
+    try:
+        (
+            response,
+            http_response,
+        ) = current_rs_api_client.api.get_workflow_retention_rules(
+            workflow_id_or_name=workflow,
+            access_token=access_token,
+        ).result()
+        if http_response.status_code == 200:
+            return response
+        else:
+            raise Exception(
+                "Expected status code 200 but replied with "
+                "{status_code}".format(status_code=http_response.status_code)
+            )
+
+    except HTTPError as e:
+        logging.debug(
+            "Workflow retention rules could not be retrieved: "
+            "\nStatus: {}\nReason: {}\n"
+            "Message: {}".format(
+                e.response.status_code, e.response.reason, e.response.json()["message"]
+            )
+        )
+        raise Exception(e.response.json()["message"])
diff --git a/reana_client/cli/__init__.py b/reana_client/cli/__init__.py
index b87b4bf..508f0c7 100644
--- a/reana_client/cli/__init__.py
+++ b/reana_client/cli/__init__.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 #
 # This file is part of REANA.
-# Copyright (C) 2017, 2018, 2019, 2020, 2021 CERN.
+# Copyright (C) 2017, 2018, 2019, 2020, 2021, 2022 CERN.
 #
 # REANA is free software; you can redistribute it and/or modify it
 # under the terms of the MIT License; see LICENSE file for more details.
@@ -13,7 +13,7 @@ import sys
 import click
 from urllib3 import disable_warnings
 
-from reana_client.cli import workflow, files, ping, secrets, quotas
+from reana_client.cli import workflow, files, ping, secrets, quotas, retention_rules
 from reana_client.utils import get_api_url
 
 DEBUG_LOG_FORMAT = (
@@ -43,6 +43,7 @@ class ReanaCLI(click.Group):
         workflow.workflow_execution_group,
         workflow.interactive_group,
         files.files_group,
+        retention_rules.retention_rules_group,
         secrets.secrets_group,
     ]
 
diff --git a/reana_client/cli/retention_rules.py b/reana_client/cli/retention_rules.py
new file mode 100644
index 0000000..885a2f9
--- /dev/null
+++ b/reana_client/cli/retention_rules.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of REANA.
+# Copyright (C) 2022 CERN.
+#
+# REANA is free software; you can redistribute it and/or modify it
+# under the terms of the MIT License; see LICENSE file for more details.
+"""REANA client retention-rules related commands."""
+
+import logging
+import sys
+from typing import Optional, Tuple
+
+import click
+
+from reana_client.api.client import get_workflow_retention_rules
+from reana_client.cli.utils import (
+    add_access_token_options,
+    add_workflow_option,
+    check_connection,
+    display_formatted_output,
+)
+from reana_client.printer import display_message
+
+
+@click.group(help="Workspace file retention commands")
+def retention_rules_group():
+    """Workspace file retention commands."""
+    pass
+
+
+@retention_rules_group.command()
+@add_workflow_option
+@add_access_token_options
+@check_connection
+@click.option(
+    "--format",
+    "_format",
+    multiple=True,
+    help="Format output according to column titles or column values. "
+    "Use `<columm_name>=<column_value>` format. "
+    "E.g. display pattern and status of active retention rules "
+    "`--format workspace_files,status=active`.",
+)
+@click.option(
+    "--json",
+    "output_format",
+    flag_value="json",
+    default=None,
+    help="Get output in JSON format.",
+)
+def retention_rules_list(
+    access_token: str, workflow: str, _format: Tuple[str], output_format: Optional[str]
+) -> None:  # noqa: D301
+    """List the retention rules for a workflow.
+
+    Example:\n
+    \t $ reana-client retention-rules-list -w myanalysis.42
+    """
+    try:
+        rules = get_workflow_retention_rules(workflow, access_token).get(
+            "retention_rules", []
+        )
+    except Exception as e:
+        logging.debug(e, exc_info=True)
+        display_message(str(e), msg_type="error")
+        sys.exit(1)
+
+    sorted_rules = sorted(rules, key=lambda rule: rule["retention_days"])
+
+    headers = ["workspace_files", "retention_days", "apply_on", "status"]
+    rows = [[rule[h] or "-" for h in headers] for rule in sorted_rules]
+    display_formatted_output(rows, headers, _format, output_format)


Test patch:
diff --git a/tests/test_cli_retention_rules.py b/tests/test_cli_retention_rules.py
new file mode 100644
index 0000000..2044c63
--- /dev/null
+++ b/tests/test_cli_retention_rules.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of REANA.
+# Copyright (C) 2022 CERN.
+#
+# REANA is free software; you can redistribute it and/or modify it
+# under the terms of the MIT License; see LICENSE file for more details.
+"""REANA client retention rules tests."""
+
+from unittest.mock import MagicMock, patch
+
+from click.testing import CliRunner
+
+from reana_client.cli import cli
+
+
+@patch("reana_client.cli.retention_rules.get_workflow_retention_rules")
+def test_retention_rules_list(mock_get_workflow_retention_rules: MagicMock):
+    """Test retention-rules-list command."""
+    workflow_id = "123456"
+    workflow_name = "workflow"
+    access_token = "secret"
+    pattern = "*.tmp"
+    mock_get_workflow_retention_rules.return_value = {
+        "workflow_id": workflow_id,
+        "workflow_name": workflow_name,
+        "retention_rules": [
+            {
+                "workspace_files": pattern,
+                "status": "created",
+                "apply_on": None,
+                "retention_days": 42,
+            },
+        ],
+    }
+
+    runner = CliRunner(env={"REANA_SERVER_URL": "localhost"})
+    result = runner.invoke(
+        cli, ["retention-rules-list", "-w", workflow_name, "-t", access_token]
+    )
+
+    assert result.exit_code == 0
+    assert pattern in result.output
+    mock_get_workflow_retention_rules.assert_called_once_with(
+        workflow_name, access_token
+    )